From 4a4d064870b1a0a9bbf23a759a20aa5ce723f5fe Mon Sep 17 00:00:00 2001 From: Kiran Vaddadi Date: Fri, 13 Mar 2026 11:20:53 +0530 Subject: [PATCH 1/6] remove kafka tls --- charts/intelligence/.helmignore | 23 - charts/intelligence/Chart.yaml | 12 - charts/intelligence/README.md | 90 ---- charts/intelligence/templates/_helpers.tpl | 170 ------- .../server/kafka-discovery-configmap.yaml | 44 -- .../templates/server/server-deployment.yml | 415 ------------------ .../templates/server/server-service.yml | 30 -- .../templates/service-accounts/role.yaml | 33 -- .../service-accounts/service-account.yml | 15 - charts/intelligence/values.yaml | 137 ------ charts/kafka/Chart.yaml | 2 +- charts/kafka/README.md | 292 +----------- charts/kafka/kafka-1.0.1.tgz | Bin 0 -> 12603 bytes charts/kafka/templates/kafka-config.yaml | 40 +- .../templates/kafka-external-services.yaml | 54 --- charts/kafka/templates/kafka-statefulset.yaml | 219 +-------- .../pre-upgrade-statefulset-cleanup.yaml | 14 +- charts/kafka/values.yaml | 59 +-- charts/portal/Chart.lock | 9 +- charts/portal/Chart.yaml | 6 +- charts/portal/README.md | 10 +- .../charts/apim-intelligence-1.0.21.tgz | Bin 10201 -> 0 bytes charts/portal/templates/_helpers.tpl | 75 ++++ charts/portal/templates/apim/apim-config.yaml | 8 +- .../portal/templates/apim/apim-service.yaml | 20 + .../portal/templates/gateway-api/gateway.yaml | 4 + .../templates/gateway-api/tlsroute.yaml | 34 ++ .../templates/ingress/contour-httpproxy.yaml | 28 ++ charts/portal/templates/ingress/ingress.yaml | 19 + charts/portal/templates/ingress/route.yaml | 21 + .../intelligence/intelligence-config.yaml} | 10 +- .../intelligence/intelligence-deployment.yaml | 175 ++++++++ .../intelligence/intelligence-role.yaml | 24 + .../intelligence-rolebinding.yaml} | 14 +- .../intelligence-service-account.yaml | 16 + .../intelligence/intelligence-service.yaml | 33 ++ .../templates/jobs/cert-update-job.yaml | 11 +- charts/portal/templates/jobs/job-secret.yaml | 3 +- charts/portal/values-production.yaml | 177 ++------ charts/portal/values.yaml | 202 +++------ 40 files changed, 637 insertions(+), 1911 deletions(-) delete mode 100644 charts/intelligence/.helmignore delete mode 100644 charts/intelligence/Chart.yaml delete mode 100644 charts/intelligence/README.md delete mode 100644 charts/intelligence/templates/_helpers.tpl delete mode 100644 charts/intelligence/templates/server/kafka-discovery-configmap.yaml delete mode 100644 charts/intelligence/templates/server/server-deployment.yml delete mode 100644 charts/intelligence/templates/server/server-service.yml delete mode 100644 charts/intelligence/templates/service-accounts/role.yaml delete mode 100644 charts/intelligence/templates/service-accounts/service-account.yml delete mode 100644 charts/intelligence/values.yaml create mode 100644 charts/kafka/kafka-1.0.1.tgz delete mode 100644 charts/kafka/templates/kafka-external-services.yaml delete mode 100644 charts/portal/charts/apim-intelligence-1.0.21.tgz rename charts/{intelligence/templates/server/server-config.yml => portal/templates/intelligence/intelligence-config.yaml} (67%) create mode 100644 charts/portal/templates/intelligence/intelligence-deployment.yaml create mode 100644 charts/portal/templates/intelligence/intelligence-role.yaml rename charts/{intelligence/templates/service-accounts/rolebinding.yaml => portal/templates/intelligence/intelligence-rolebinding.yaml} (51%) create mode 100644 charts/portal/templates/intelligence/intelligence-service-account.yaml create mode 100644 charts/portal/templates/intelligence/intelligence-service.yaml diff --git a/charts/intelligence/.helmignore b/charts/intelligence/.helmignore deleted file mode 100644 index 0e8a0eb3..00000000 --- a/charts/intelligence/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/charts/intelligence/Chart.yaml b/charts/intelligence/Chart.yaml deleted file mode 100644 index 8bcaeb41..00000000 --- a/charts/intelligence/Chart.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v2 -appVersion: "1.0.0" -description: APIM Intelligence -name: apim-intelligence -version: 1.0.21 -type: application -home: https://github.com/CAAPIM/apim-chart -maintainers: - - name: Layer7 -sources: - - https://github.com/CAAPIM/apim-charts -engine: gotpl diff --git a/charts/intelligence/README.md b/charts/intelligence/README.md deleted file mode 100644 index e50603b8..00000000 --- a/charts/intelligence/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# API Management Intelligence Chart - -This chart deploys the API Management Intelligence. APIM Intelligence is responsible for processing and aggregating policy code from the API Gateway and making it available to the Portal. - -## Parameters - -### Global parameters - -These values are typically provided by a parent chart. - -| Parameter | Description | Default | -| --- | --- | --- | -| `global.portalRepository` | The repository for the portal images. | `caapim/` | -| `global.pullSecret` | The name of an existing image pull secret. | `""` | -| `global.databaseType` | The type of database to use. | `mysql` | -| `global.databaseSecret` | The name of the secret containing the database password. | `intelligence-db-secret` | -| `global.databaseUsername` | The username for the database. | `intelligence_user` | -| `global.databaseHost` | The hostname of the database. | `mysql` | -| `global.databasePort` | The port of the database. | `3306` | -| `global.databaseUseSSL` | Whether to use SSL for the database connection. | `true` | -| `global.databaseRequireSSL` | Whether to require SSL for the database connection. | `false` | -| `global.legacyDatabaseNames` | Whether to use legacy database names. | `false` | -| `global.subdomainPrefix` | The prefix for the subdomain. | `dev-portal` | -| `global.podSecurityContext` | The security context for the pod. | `{}` | -| `global.containerSecurityContext` | The security context for the container. | `{}` | -| `global.schedulerName` | The name of the scheduler to use for the pods. | `""` | -| `global.additionalLabels` | Additional labels to be applied to all resources. | `{}` | - -### General parameters - -| Parameter | Description | Default | -| --- | --- | --- | -| `nameOverride` | A string to override the name of the chart. | `""` | -| `fullnameOverride` | A string to override the full name of the chart. | `""` | -| `forceRedeploy` | Whether to force redeployment of statefulsets and deployments on upgrade. | `false` | - -### Intelligence Server settings - -| Parameter | Description | Default | -| --- | --- | --- | - -| `image.intelligenceServer` | The image for the intelligence-server. | `intelligence-server:latest` | -| `image.pullPolicy` | The pull policy for the intelligence-server image. | `IfNotPresent` | -| `image.autoDiscovery` | The image for auto-discovery init container (Kafka broker discovery). | `tls-automator:5.4` | -| `serviceAccount.create` | Whether to create a service account. | `true` | -| `serviceAccount.name` | The name of the service account to use. | `""` | -| `serviceAccount.automountServiceAccountToken` | Whether to automount the service account token. | `true` | -| `rbac.create` | Whether to create RBAC resources. | `true` | -| `intelligenceServer.replicaCount` | The number of replicas for the intelligence-server. | `1` | -| `intelligenceServer.additionalLabels` | Additional labels for the intelligence-server deployment. | `{}` | -| `intelligenceServer.affinity` | The affinity for the intelligence-server pods. | `{}` | -| `intelligenceServer.nodeSelector` | The node selector for the intelligence-server pods. | `{}` | -| `intelligenceServer.tolerations` | The tolerations for the intelligence-server pods. | `[]` | -| `intelligenceServer.podSecurityContext` | The security context for the intelligence-server pod. | `{}` | -| `intelligenceServer.containerSecurityContext` | The security context for the intelligence-server container. | `{}` | -| `intelligenceServer.resources` | The resource requests and limits for the intelligence-server pods. | `{}` | -| `intelligenceServer.service.type` | The type of service for the intelligence-server. | `ClusterIP` | -| `intelligenceServer.service.port` | The port for the intelligence-server service. | `8282` | -| `intelligenceServer.service.sessionAffinity` | The session affinity for the intelligence-server service. | `None` | -| `intelligenceServer.service.externalTrafficPolicy` | The external traffic policy for the intelligence-server service. | `""` | -| `intelligenceServer.service.internalTrafficPolicy` | The internal traffic policy for the intelligence-server service. | `""` | -| `intelligenceServer.portalDataHost` | The host and port for the portal data service. | `"portal-data:8080"` | - -### Kafka settings - -The Intelligence Server requires a connection to a Kafka cluster. The following settings are used to configure the connection. - -| Parameter | Description | Default | -| --- | --- | --- | -| `intelligenceServer.kafka.autoDiscovery.enabled` | When enabled, an initContainer is used to discover Kafka broker external addresses. | `true` | -| `intelligenceServer.kafka.autoDiscovery.image.repository` | The repository for the auto-discovery initContainer image. | `docker.io/bitnami/kubectl` | -| `intelligenceServer.kafka.autoDiscovery.image.tag` | The tag for the auto-discovery initContainer image. | `1.33.3-debian-12-r0` | -| `intelligenceServer.kafka.autoDiscovery.image.pullPolicy` | The pull policy for the auto-discovery initContainer image. | `IfNotPresent` | -| `intelligenceServer.kafka.autoDiscovery.resources` | The resource requests and limits for the auto-discovery initContainer. | `{}` | -| `intelligenceServer.kafka.externalAdvertisedBrokers` | A comma-separated list of external advertised Kafka brokers. | `""` | -| `intelligenceServer.kafka.kafkaCa.caSecretName` | The name of the secret containing the Kafka CA certificate. | `""` | -| `intelligenceServer.kafka.kafkaCa.passwordSecretName` | The name of the secret containing the password for the Kafka CA certificate. | `""` | -| `intelligenceServer.kafka.kafkaCa.passwordSecretKey` | The key in the password secret that contains the password. | `keypass.txt` | - -**NOTE:** When this chart is used as a subchart, it requires Kafka configuration for its initContainer to discover brokers. The parent chart must provide these values under the `apim-intelligence.kafka` key. - -To avoid duplication, the parent chart's `values.yaml` can use a YAML alias: - -```yaml -kafka: &kafka_config - #... kafka subchart values - -apim-intelligence: - kafka: *kafka_config -``` \ No newline at end of file diff --git a/charts/intelligence/templates/_helpers.tpl b/charts/intelligence/templates/_helpers.tpl deleted file mode 100644 index 20683d09..00000000 --- a/charts/intelligence/templates/_helpers.tpl +++ /dev/null @@ -1,170 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "intelligence.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "intelligence.fullname" -}} -{{- if .Values.fullnameOverride -}} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- if contains $name .Release.Name -}} -{{- .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} -{{- end -}} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "intelligence.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} -{{- end -}} - - -{{/* - Set the service account name for the Intelligence Server - */}} -{{- define "intelligence.serviceAccountName" -}} -{{- if .Values.global.serviceAccountName }} - {{ default "default" .Values.global.serviceAccountName }} -{{- else }} -{{- if .Values.serviceAccount.create -}} - {{ default (include "intelligence.fullname" .) .Values.serviceAccount.name }} -{{- else -}} - {{ default "default" .Values.serviceAccount.name }} -{{- end -}} -{{- end -}} -{{- end -}} - - -{{/* -Get "intelligence" database name -*/}} -{{- define "intelligence-db-name" -}} - {{ if .Values.global.legacyDatabaseNames }} - {{- print "intelligence" }} - {{- else }} - {{- $f:= .Values.global.subdomainPrefix -}} - {{ if empty $f }} - {{- fail "Please define subdomainPrefix in values.yaml" }} - {{- else }} - {{- printf "%s_%s" $f "intelligence" | replace "-" "_" -}} - {{- end }} - {{- end }} -{{- end -}} - -{{/* -Get "database-port" based on databaseType value -*/}} -{{- define "database-port" -}} - {{- print .Values.global.databasePort -}} -{{- end -}} - -{{/* -Get "kafka" brokers -*/}} -{{- define "kafka-brokers" -}} - {{- $kafkaName := "kafka" -}} - {{- if .Values.kafka.fullnameOverride -}} - {{- $kafkaName = .Values.kafka.fullnameOverride -}} - {{- else -}} - {{- $kafkaName = printf "%s-kafka" .Release.Name -}} - {{- end -}} - {{- if and .Values.kafka.kafka .Values.kafka.kafka.listeners }} - {{- /* Custom Kafka subchart */ -}} - {{- printf "%s:%g" $kafkaName .Values.kafka.kafka.listeners.internal.port -}} - {{- else if and .Values.kafka.listeners .Values.kafka.listeners.client }} - {{- /* Bitnami Kafka chart */ -}} - {{- printf "%s:%g" $kafkaName .Values.kafka.listeners.client.containerPort -}} - {{- else }} - {{- /* Default fallback */ -}} - {{- printf "%s:9092" $kafkaName -}} - {{- end }} -{{- end -}} - -{{/* -Create Image Pull Secret -*/}} -{{- define "intelligence-imagePullSecret" }} -{{- if and (not .Values.useExistingPullSecret) (.Values.imagePullSecret.enabled) }} -{{- printf "{\"auths\":{\"%s\":{\"username\":\"%s\",\"password\":\"%s\",\"auth\":\"%s\"}}}" .Values.global.portalRepository .Values.imagePullSecret.username .Values.imagePullSecret.password (printf "%s:%s" .Values.imagePullSecret.username .Values.imagePullSecret.password | b64enc) | b64enc }} -{{- end }} -{{- end }} - -{{- define "intelligence.validate" -}} -{{- $messages := list -}} -{{- $messages := append $messages (include "intelligence.validateValues.autoDiscoveryRBAC" .) -}} -{{- $message := join "\n" $messages -}} -{{- if $message -}} -{{- printf "\nVALUES VALIDATION:\n%s" $message | fail -}} -{{- end -}} -{{- end -}} - -{{/* Validate values of intelligence - RBAC should be enabled when autoDiscovery is enabled */}} -{{- define "intelligence.validateValues.autoDiscoveryRBAC" -}} -{{- if and .Values.intelligenceServer.kafka.autoDiscovery.enabled (not .Values.rbac.create ) }} -intelligence: rbac-create - By specifying ".Values.intelligenceServer.kafka.autoDiscovery.enabled=true" - an initContainer will be used to auto-detect the external IPs/ports by querying the - K8s API. Please note this initContainer requires specific RBAC resources. -{{- end -}} -{{- if and .Values.intelligenceServer.kafka.autoDiscovery.enabled (not .Values.serviceAccount.automountServiceAccountToken) }} -intelligence: serviceAccount-automountServiceAccountToken - By specifying ".Values.intelligenceServer.kafka.autoDiscovery.enabled=true" - an initContainer will be used to auto-detect the external IPs/ports by querying the - K8s API. Please note this initContainer requires the service account token. Please set serviceAccount.automountServiceAccountToken=true -{{- end -}} -{{- end -}} - -{{/* -Generate Intelligence public host based on global configurations -*/}} -{{- define "intelligence.publicHost" -}} - {{- /* When deployed as subchart, check if portal.domain is set and use it */ -}} - {{- /* This allows Jenkins to set portal.domain without also setting global.domain */ -}} - {{- $domain := "dev.ca.com" -}} - {{- if .Values.portal -}} - {{- if .Values.portal.domain -}} - {{- /* Subchart: use portal.domain if explicitly set */ -}} - {{- $domain = .Values.portal.domain -}} - {{- else -}} - {{- /* Fallback to global.domain */ -}} - {{- $domain = default "dev.ca.com" .Values.global.domain -}} - {{- end -}} - {{- else -}} - {{- /* Standalone: use global.domain */ -}} - {{- $domain = default "dev.ca.com" .Values.global.domain -}} - {{- end -}} - {{- $subdomainPrefix := default "dev-portal" .Values.global.subdomainPrefix -}} - {{- $defaultTenantId := default "apim" .Values.global.defaultTenantId -}} - {{- if .Values.global.legacyHostnames }} - {{- printf "%s-%s.%s" $defaultTenantId "ssg" $domain -}} - {{- else if .Values.global.saas }} - {{- printf "apim-ssg-%s.%s" $subdomainPrefix $domain -}} - {{- else }} - {{- printf "%s-ssg.%s" $subdomainPrefix $domain -}} - {{- end }} -{{- end -}} - -{{/* -Generate Intelligence public port -Defaults to 443 for HTTPS, or from global.papiPublicPort if set -*/}} -{{- define "intelligence.publicPort" -}} - {{- if .Values.global.papiPublicPort }} - {{- .Values.global.papiPublicPort -}} - {{- else }} - {{- "443" -}} - {{- end }} -{{- end -}} \ No newline at end of file diff --git a/charts/intelligence/templates/server/kafka-discovery-configmap.yaml b/charts/intelligence/templates/server/kafka-discovery-configmap.yaml deleted file mode 100644 index 2db93e52..00000000 --- a/charts/intelligence/templates/server/kafka-discovery-configmap.yaml +++ /dev/null @@ -1,44 +0,0 @@ -{{- if .Values.intelligenceServer.kafka.autoDiscovery.enabled }} -# -# Kafka Discovery ConfigMap -# -# Purpose: -# This ConfigMap stores dynamically discovered Kafka broker information that is -# populated by the kafka-broker-discovery init container at runtime. -# -# What it contains: -# - KAFKA_EXTERNAL_ADVERTIZED_BROKERS: Comma-separated list of Kafka broker IPs/hostnames -# discovered from LoadBalancer services (e.g., "10.252.148.75:9094,10.252.148.43:9094") -# - PAPI_PUBLIC_HOST: The public hostname for the API gateway, constructed from the -# namespace and portal domain (e.g., "mr6-ssg.gke-us-west-1-cluster-01.apim.broadcom.net") -# -# How it works: -# 1. Init container discovers Kafka LoadBalancer IPs and portal domain at pod startup -# 2. Init container creates/updates this ConfigMap with the discovered values -# 3. Main intelligence-server container reads these values via envFrom -# 4. Intelligence server uses these values to provide Kafka connection info to clients -# -# Why ConfigMap instead of files: -# - Works with distroless images (no shell required) -# - Kubernetes-native approach -# - Easy to inspect with kubectl -# - No volume mount dependencies -# -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "intelligence.fullname" . }}-kafka-discovery - namespace: {{ .Release.Namespace }} - labels: - app: intelligence-server - component: kafka-discovery - chart: {{ template "intelligence.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} - annotations: - "helm.sh/hook": "pre-install,pre-upgrade" - "helm.sh/hook-delete-policy": "before-hook-creation" - "helm.sh/hook-weight": "-5" -data: - KAFKA_EXTERNAL_ADVERTIZED_BROKERS: "" -{{- end }} \ No newline at end of file diff --git a/charts/intelligence/templates/server/server-deployment.yml b/charts/intelligence/templates/server/server-deployment.yml deleted file mode 100644 index c48863a4..00000000 --- a/charts/intelligence/templates/server/server-deployment.yml +++ /dev/null @@ -1,415 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: intelligence-server - labels: - app: intelligence-server - chart: {{ template "intelligence.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} - {{- range $key, $val := .Values.global.additionalLabels }} - {{ $key }}: "{{ $val }}" - {{- end }} - {{- range $key, $val := .Values.intelligenceServer.additionalLabels }} - {{ $key }}: "{{ $val }}" - {{- end }} -spec: - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - replicas: {{ .Values.intelligenceServer.replicaCount }} - selector: - matchLabels: - app: intelligence-server - template: - metadata: - labels: - app: intelligence-server - release: {{ .Release.Name }} - {{- if .Values.intelligenceServer.podAnnotations }} - annotations: {{- toYaml .Values.intelligenceServer.podAnnotations | nindent 8 }} - {{- if .Values.intelligenceServer.forceRedeploy }} - timestamp: {{ now | quote }} - {{- end }} - {{- end }} - {{- if not .Values.intelligenceServer.podAnnotations }} - {{- if .Values.intelligenceServer.forceRedeploy }} - annotations: - timestamp: {{ now | quote }} - {{- end }} - {{- end }} - spec: - serviceAccountName: {{ include "intelligence.serviceAccountName" . }} - automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} - {{- if .Values.intelligenceServer.affinity }} - affinity: {{- toYaml .Values.intelligenceServer.affinity | nindent 12 }} - {{- end }} - {{- if .Values.intelligenceServer.nodeSelector }} - nodeSelector: {{- toYaml .Values.intelligenceServer.nodeSelector | nindent 12 }} - {{- end }} - {{- if .Values.intelligenceServer.tolerations }} - tolerations: {{- toYaml .Values.intelligenceServer.tolerations | nindent 12 }} - {{- end }} - {{- if .Values.intelligenceServer.podSecurityContext }} - securityContext: {{- toYaml .Values.intelligenceServer.podSecurityContext | nindent 12 }} - {{- else if .Values.global.podSecurityContext }} - securityContext: {{- toYaml .Values.global.podSecurityContext | nindent 12 }} - {{- end }} - {{- if .Values.intelligenceServer.kafka.autoDiscovery.enabled }} - {{ template "intelligence.validate" .}} - initContainers: - - name: kafka-broker-discovery - image: "{{ .Values.global.portalRepository }}{{ .Values.image.autoDiscovery }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - command: - - /bin/sh - - -ec - - | - # This script discovers the external addresses of Kafka brokers - # and writes them to a shared volume for the main container to use. - RELEASE_NAME="{{ .Release.Name }}" - BROKER_LIST="" - - # Function to retry a command (sh-compatible) - retry_while() { - cmd="${1:?cmd is missing}" - retries="${2:-12}" - sleep_time="${3:-5}" - return_value=1 - i=1 - while [ "$i" -le "$retries" ]; do - if eval "$cmd"; then - return_value=0 - break - fi - echo "Retrying in ${sleep_time}s... (${i}/${retries})" - sleep "$sleep_time" - i=$((i + 1)) - done - return $return_value - } - - # Function to get LoadBalancer IP or Hostname from a service (sh-compatible) - k8s_svc_lb_ip() { - service_ip=$(kubectl get svc "$1" -n "$2" -o jsonpath="{.status.loadBalancer.ingress[0].ip}" 2>/dev/null || echo "") - service_hostname=$(kubectl get svc "$1" -n "$2" -o jsonpath="{.status.loadBalancer.ingress[0].hostname}" 2>/dev/null || echo "") - if [ -n "$service_ip" ]; then - echo "$service_ip" - elif [ -n "$service_hostname" ]; then - echo "$service_hostname" - else - return 1 - fi - } - - discover_broker() { - SVC_NAME=$1 - BROKER_ENDPOINT="" - - echo "Discovering broker: ${SVC_NAME}" - - if ! kubectl get svc "$SVC_NAME" -n "$MY_POD_NAMESPACE" > /dev/null 2>&1; then - echo "Service $SVC_NAME not found, skipping." - return - fi - - AUTODISCOVERY_SERVICE_TYPE=$(kubectl get svc "$SVC_NAME" -n "$MY_POD_NAMESPACE" -o jsonpath='{.spec.type}' 2>/dev/null || echo "") - - if [ "$AUTODISCOVERY_SERVICE_TYPE" = "LoadBalancer" ]; then - if ! retry_while "kubectl get svc $SVC_NAME -n $MY_POD_NAMESPACE -o jsonpath='{.status.loadBalancer.ingress[0]}'" ; then - echo "Timed out waiting for LoadBalancer IP for service $SVC_NAME" - exit 1 - fi - EXTERNAL_ADDRESS=$(k8s_svc_lb_ip "$SVC_NAME" "$MY_POD_NAMESPACE") - EXTERNAL_PORT=$(kubectl get svc "$SVC_NAME" -n "$MY_POD_NAMESPACE" -o jsonpath="{.spec.ports[0].port}" 2>/dev/null || echo "") - BROKER_ENDPOINT="${EXTERNAL_ADDRESS}:${EXTERNAL_PORT}" - elif [ "$AUTODISCOVERY_SERVICE_TYPE" = "NodePort" ]; then - NODE_PORT=$(kubectl get svc "$SVC_NAME" -n "$MY_POD_NAMESPACE" -o jsonpath="{.spec.ports[0].nodePort}" 2>/dev/null || echo "") - NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="ExternalIP")].address}' 2>/dev/null || kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null || echo "") - if [ -n "$NODE_IP" ] && [ -n "$NODE_PORT" ]; then - BROKER_ENDPOINT="${NODE_IP}:${NODE_PORT}" - fi - fi - - if [ -n "$BROKER_ENDPOINT" ]; then - if [ -z "$BROKER_LIST" ]; then - BROKER_LIST="$BROKER_ENDPOINT" - else - BROKER_LIST="$BROKER_LIST,$BROKER_ENDPOINT" - fi - fi - } - - # Discover Kafka broker nodes - Aligned with StatefulSet ordinals - # For custom Kafka subchart - {{- if and .Values.kafka (hasKey .Values.kafka "kafka") .Values.kafka.kafka .Values.kafka.kafka.replicaCount }} - BROKER_COUNT={{ .Values.kafka.kafka.replicaCount }} - {{- $ordinalStart := 0 }} - {{- if and .Values.kafka.kafka.ordinals (hasKey .Values.kafka.kafka.ordinals "start") }} - {{- $ordinalStart = int .Values.kafka.kafka.ordinals.start }} - {{- end }} - ORDINAL_START={{ $ordinalStart }} - # Use kafka.fullnameOverride if set, otherwise construct from release name - {{- $kafkaFullname := .Values.kafka.fullnameOverride | default (printf "%s-kafka" .Release.Name) }} - KAFKA_FULLNAME="{{ $kafkaFullname }}" - for i in $(seq $ORDINAL_START $((ORDINAL_START + BROKER_COUNT - 1))); do - SVC_NAME="${KAFKA_FULLNAME}-${i}-external" - discover_broker "$SVC_NAME" - done - {{- else if and .Values.kafka (hasKey .Values.kafka "controller") .Values.kafka.controller }} - # For Bitnami Kafka chart (backward compatibility) - {{- $kafkaFullname := .Values.kafka.fullnameOverride | default (printf "%s-kafka" .Release.Name) }} - KAFKA_FULLNAME="{{ $kafkaFullname }}" - {{- if and .Values.kafka.controller.replicaCount (not .Values.kafka.controller.controllerOnly) }} - CONTROLLER_BROKER_COUNT={{ .Values.kafka.controller.replicaCount }} - for i in $(seq 0 $((CONTROLLER_BROKER_COUNT - 1))); do - SVC_NAME="${KAFKA_FULLNAME}-controller-${i}-external" - discover_broker "$SVC_NAME" - done - {{- end }} - - # Discover dedicated broker nodes - {{- if and (hasKey .Values.kafka "broker") .Values.kafka.broker .Values.kafka.broker.replicaCount }} - BROKER_COUNT={{ .Values.kafka.broker.replicaCount }} - if [ "$BROKER_COUNT" -gt 0 ]; then - for i in $(seq 0 $((BROKER_COUNT - 1))); do - SVC_NAME="${KAFKA_FULLNAME}-broker-${i}-external" - discover_broker "$SVC_NAME" - done - fi - {{- end }} - {{- else }} - # Fallback: No Kafka configuration found - echo "Warning: No Kafka broker configuration found" - {{- end }} - - echo "Discovered Kafka brokers: ${BROKER_LIST}" - - # Discover portal domain from portal-data ConfigMap - PORTAL_DOMAIN="" - PORTAL_CONFIGMAP="portal-data-config" - if kubectl get configmap "$PORTAL_CONFIGMAP" -n "$MY_POD_NAMESPACE" >/dev/null 2>&1; then - PORTAL_DOMAIN=$(kubectl get configmap "$PORTAL_CONFIGMAP" -n "$MY_POD_NAMESPACE" -o jsonpath='{.data.PORTAL_SUBDOMAIN}' 2>/dev/null || echo "") - if [ -n "$PORTAL_DOMAIN" ]; then - echo "Discovered portal domain: ${PORTAL_DOMAIN}" - else - echo "Warning: PORTAL_SUBDOMAIN not found in ${PORTAL_CONFIGMAP}, using default" - fi - else - echo "Warning: ConfigMap ${PORTAL_CONFIGMAP} not found, PAPI_PUBLIC_HOST may not be set correctly" - fi - - # Construct PAPI_PUBLIC_HOST from namespace and discovered domain - # Format: {namespace}-ssg.{domain} - if [ -n "$PORTAL_DOMAIN" ]; then - PAPI_PUBLIC_HOST="${MY_POD_NAMESPACE}-ssg.${PORTAL_DOMAIN}" - echo "PAPI_PUBLIC_HOST will be set to: ${PAPI_PUBLIC_HOST}" - else - echo "ERROR: Could not discover portal domain from ${PORTAL_CONFIGMAP}" - echo "PAPI_PUBLIC_HOST will not be set" - PAPI_PUBLIC_HOST="" - fi - - # Update ConfigMap for distroless compatibility - CONFIGMAP_NAME="{{ include "intelligence.fullname" . }}-kafka-discovery" - echo "Updating ConfigMap ${CONFIGMAP_NAME}..." - - if [ -n "$PAPI_PUBLIC_HOST" ]; then - # Both Kafka brokers and PAPI_PUBLIC_HOST discovered - if kubectl get configmap "$CONFIGMAP_NAME" -n "$MY_POD_NAMESPACE" >/dev/null 2>&1; then - kubectl patch configmap "$CONFIGMAP_NAME" -n "$MY_POD_NAMESPACE" \ - --patch "{\"data\":{\"KAFKA_EXTERNAL_ADVERTIZED_BROKERS\":\"${BROKER_LIST}\",\"PAPI_PUBLIC_HOST\":\"${PAPI_PUBLIC_HOST}\"}}" - echo "ConfigMap ${CONFIGMAP_NAME} updated successfully" - else - kubectl create configmap "$CONFIGMAP_NAME" -n "$MY_POD_NAMESPACE" \ - --from-literal="KAFKA_EXTERNAL_ADVERTIZED_BROKERS=${BROKER_LIST}" \ - --from-literal="PAPI_PUBLIC_HOST=${PAPI_PUBLIC_HOST}" - echo "ConfigMap ${CONFIGMAP_NAME} created successfully" - fi - else - # Only Kafka brokers discovered (PAPI_PUBLIC_HOST discovery failed) - if kubectl get configmap "$CONFIGMAP_NAME" -n "$MY_POD_NAMESPACE" >/dev/null 2>&1; then - kubectl patch configmap "$CONFIGMAP_NAME" -n "$MY_POD_NAMESPACE" \ - --patch "{\"data\":{\"KAFKA_EXTERNAL_ADVERTIZED_BROKERS\":\"${BROKER_LIST}\"}}" - echo "ConfigMap ${CONFIGMAP_NAME} updated (without PAPI_PUBLIC_HOST)" - else - kubectl create configmap "$CONFIGMAP_NAME" -n "$MY_POD_NAMESPACE" \ - --from-literal="KAFKA_EXTERNAL_ADVERTIZED_BROKERS=${BROKER_LIST}" - echo "ConfigMap ${CONFIGMAP_NAME} created (without PAPI_PUBLIC_HOST)" - fi - fi - - # Keep file for backward compatibility - echo "export KAFKA_EXTERNAL_ADVERTIZED_BROKERS=${BROKER_LIST}" > /shared/kafka.env - if [ -n "$PAPI_PUBLIC_HOST" ]; then - echo "export PAPI_PUBLIC_HOST=${PAPI_PUBLIC_HOST}" >> /shared/kafka.env - fi - echo "Kafka discovery completed successfully" - env: - - name: MY_POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - volumeMounts: - - name: shared-data - mountPath: /shared - {{- end }} - containers: - - name: intelligence-server - image: "{{ .Values.global.portalRepository }}{{ .Values.image.intelligenceServer }}" - imagePullPolicy: "{{ .Values.image.pullPolicy }}" - {{- if .Values.intelligenceServer.containerSecurityContext }} - securityContext: {{- toYaml .Values.intelligenceServer.containerSecurityContext | nindent 12 }} - {{- else if .Values.global.containerSecurityContext }} - securityContext: {{- toYaml .Values.global.containerSecurityContext | nindent 12 }} - {{- end }} - # Distroless compatible: no shell, direct Java execution - command: ["java"] - args: ["-jar", "/opt/app/intelligence-server.jar"] - env: - - name: RABBITMQ_DEFAULT_PASS - valueFrom: - secretKeyRef: - name: {{ .Values.rabbitmq.auth.secretName }} - key: rabbitmq-password - optional: false - - name: INTELLIGENCE_DATABASE_NAME - value: {{ template "intelligence-db-name" . }} - - name: DATABASE_USERNAME - value: {{ .Values.global.databaseUsername | quote }} - - name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.global.databaseSecret }} - {{ if eq .Values.global.databaseType "mysql" }} - key: mysql-password - {{- end }} - # Internal Kafka connection (for intelligence-server's own use) - - name: KAFKA_BROKERS - value: {{ .Values.intelligenceServer.kafka.brokers | default "kafka:9092" | quote }} - - - name: KAFKA_SECURITY_PROTOCOL - value: {{ .Values.intelligenceServer.kafka.securityProtocol | default "PLAINTEXT" | quote }} - # Certificates that intelligence-server provides to third-party clients - {{- if .Values.intelligenceServer.kafka.kafkaCa.caSecretName }} - - name: KAFKA_CA_CERTIFICATE - valueFrom: - secretKeyRef: - name: {{ .Values.intelligenceServer.kafka.kafkaCa.caSecretName }} - key: {{ .Values.intelligenceServer.kafka.kafkaCa.caCertKey | default "apim-ssl.crt" }} - optional: false - - name: KAFKA_CA_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.intelligenceServer.kafka.kafkaCa.caSecretName }} - key: {{ .Values.intelligenceServer.kafka.kafkaCa.caKeyKey | default "apim-ssl.key" }} - optional: false - - name: KAFKA_CA_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.intelligenceServer.kafka.kafkaCa.passwordSecretName }} - key: {{ .Values.intelligenceServer.kafka.kafkaCa.passwordSecretKey | default "keypass.txt" }} - optional: false - - name: TSSG_SSL_KEY_PASS - valueFrom: - secretKeyRef: - name: {{ .Values.intelligenceServer.kafka.kafkaCa.passwordSecretName }} - key: {{ .Values.intelligenceServer.kafka.kafkaCa.passwordSecretKey | default "keypass.txt" }} - optional: false - {{- else }} - # No certificates provided - - name: KAFKA_CA_CERTIFICATE - value: "" - - name: KAFKA_CA_KEY - value: "" - - name: KAFKA_CA_PASSWORD - value: "" - - name: TSSG_SSL_KEY_PASS - value: "" - {{- end }} - # Kafka External Advertised Brokers (for user-facing connections) - {{- if not .Values.intelligenceServer.kafka.autoDiscovery.enabled }} - {{- if .Values.intelligenceServer.kafka.externalAdvertisedBrokers }} - - name: KAFKA_EXTERNAL_ADVERTIZED_BROKERS - value: {{ .Values.intelligenceServer.kafka.externalAdvertisedBrokers | quote }} - {{- end }} - {{- end }} - {{- if .Values.intelligenceServer.env }} - {{- range $key, $value := .Values.intelligenceServer.env }} - - name: {{ $key }} - value: {{ $value | quote }} - {{- end }} - {{- end }} - {{- if .Values.intelligenceServer.kafka.autoDiscovery.enabled }} - # Read PAPI_PUBLIC_HOST from ConfigMap populated by kafka-broker-discovery init container - # The init container discovers the portal domain from portal-data ConfigMap at runtime - # Format: {namespace}-ssg.{domain} - - name: PAPI_PUBLIC_HOST - valueFrom: - configMapKeyRef: - name: {{ include "intelligence.fullname" . }}-kafka-discovery - key: PAPI_PUBLIC_HOST - - name: PAPI_PUBLIC_PORT - value: {{ include "intelligence.publicPort" . | quote }} - {{- end }} - envFrom: - # Always include the main intelligence-server config - - configMapRef: - name: intelligence-server-config - {{- if .Values.intelligenceServer.kafka.autoDiscovery.enabled }} - # Include discovered Kafka broker IPs when auto-discovery is enabled - - configMapRef: - name: {{ include "intelligence.fullname" . }}-kafka-discovery - optional: true - {{- end }} - readinessProbe: - httpGet: - path: "/health/readiness" - port: 8282 - initialDelaySeconds: 90 - timeoutSeconds: 1 - periodSeconds: 15 - successThreshold: 1 - livenessProbe: - httpGet: - path: "/health/liveness" - port: 8282 - initialDelaySeconds: 120 - timeoutSeconds: 1 - periodSeconds: 15 - successThreshold: 1 - {{- if .Values.intelligenceServer.resources }} - resources: {{- toYaml .Values.intelligenceServer.resources | nindent 12 }} - {{- end }} - {{- if .Values.intelligenceServer.kafka.autoDiscovery.enabled }} - volumeMounts: - - name: shared-data - mountPath: /shared - {{- end }} - ports: - - containerPort: 8282 - volumes: - {{- if .Values.intelligenceServer.kafka.autoDiscovery.enabled }} - - name: shared-data - emptyDir: {} - {{- end }} - {{- if .Values.global.pullSecret }} - imagePullSecrets: - - name: "{{ .Values.global.pullSecret }}" - {{- end }} - {{- if .Values.intelligenceServer.affinity }} - affinity: {{- toYaml .Values.intelligenceServer.affinity | nindent 8 }} - {{- end }} - {{- if .Values.intelligenceServer.nodeSelector }} - nodeSelector: {{- toYaml .Values.intelligenceServer.nodeSelector | nindent 8 }} - {{- end }} - {{- if .Values.intelligenceServer.tolerations }} - tolerations: {{- toYaml .Values.intelligenceServer.tolerations | nindent 8 }} - {{- end }} - {{- if .Values.global.schedulerName }} - schedulerName: "{{ .Values.global.schedulerName }}" - {{- end }} - restartPolicy: Always - terminationGracePeriodSeconds: 30 \ No newline at end of file diff --git a/charts/intelligence/templates/server/server-service.yml b/charts/intelligence/templates/server/server-service.yml deleted file mode 100644 index 01ad2239..00000000 --- a/charts/intelligence/templates/server/server-service.yml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: intelligence-server - labels: - app: intelligence-server - chart: {{ template "intelligence.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - type: {{ .Values.intelligenceServer.service.type }} - ports: - - name: http - port: {{ .Values.intelligenceServer.service.port }} - targetPort: 8282 - selector: - app: intelligence-server - release: {{ .Release.Name }} - {{- if .Values.intelligenceServer.service.sessionAffinity }} - sessionAffinity: {{ .Values.intelligenceServer.service.sessionAffinity }} - {{- end }} - {{- if .Values.intelligenceServer.service.sessionAffinityConfig }} - sessionAffinityConfig: {{- toYaml .Values.intelligenceServer.service.sessionAffinityConfig | nindent 4 }} - {{- end }} - {{- if .Values.intelligenceServer.service.externalTrafficPolicy }} - externalTrafficPolicy: {{ .Values.intelligenceServer.service.externalTrafficPolicy }} - {{- end }} - {{- if .Values.intelligenceServer.service.internalTrafficPolicy }} - internalTrafficPolicy: {{ .Values.intelligenceServer.service.internalTrafficPolicy }} - {{- end }} \ No newline at end of file diff --git a/charts/intelligence/templates/service-accounts/role.yaml b/charts/intelligence/templates/service-accounts/role.yaml deleted file mode 100644 index 07576171..00000000 --- a/charts/intelligence/templates/service-accounts/role.yaml +++ /dev/null @@ -1,33 +0,0 @@ -{{- if and .Values.rbac.create .Values.serviceAccount.create (not .Values.global.serviceAccountName) }} -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: {{ include "intelligence.fullname" . }} - namespace: {{ .Release.Namespace }} - labels: - app: {{ template "intelligence.name" . }} - chart: {{ template "intelligence.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -rules: - - apiGroups: - - "" - resources: - - services - verbs: - - get - - list - - watch - {{- if .Values.intelligenceServer.kafka.autoDiscovery.enabled }} - - apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - create - - update - - patch - {{- end }} -{{- end }} \ No newline at end of file diff --git a/charts/intelligence/templates/service-accounts/service-account.yml b/charts/intelligence/templates/service-accounts/service-account.yml deleted file mode 100644 index 2c798f0a..00000000 --- a/charts/intelligence/templates/service-accounts/service-account.yml +++ /dev/null @@ -1,15 +0,0 @@ -{{- if and .Values.serviceAccount.create (not .Values.global.serviceAccountName) }} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "intelligence.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} - labels: - app: {{ template "intelligence.name" . }} - chart: {{ template "intelligence.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -secrets: - - name: {{ include "intelligence.fullname" . }} -automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} -{{- end }} \ No newline at end of file diff --git a/charts/intelligence/values.yaml b/charts/intelligence/values.yaml deleted file mode 100644 index 2bc9848c..00000000 --- a/charts/intelligence/values.yaml +++ /dev/null @@ -1,137 +0,0 @@ -# Default values for intelligence. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -nameOverride: "" -fullnameOverride: "" - -# Force redeployment of statefulsets and deployments on upgrade -forceRedeploy: false - -# Image configuration -# Note: When used as a subchart, these can be overridden by parent chart values -image: - autoDiscovery: "tls-automator:latest" # Image used for Kafka broker discovery init container - intelligenceServer: "intelligence-server:latest" - pullPolicy: IfNotPresent - -# ServiceAccount configuration -serviceAccount: - create: true - name: "" - automountServiceAccountToken: true - -# RBAC configuration -rbac: - create: true - -# RabbitMQ configuration -# Note: When used as a subchart, these values are typically provided by the parent chart -rabbitmq: - auth: - secretName: "rabbitmq" - username: "user" - service: - port: 5672 - -# Kafka configuration -# Note: When used as a subchart, kafka configuration is typically provided by the parent chart -# This section is required for the kafka-brokers helper template -kafka: - fullnameOverride: "kafka" - -# Global values that are typically provided by a parent chart. -# These are included here to allow the chart to be deployed standalone. -global: - portalRepository: caapim/ - pullSecret: "" # Name of an existing image pull secret - # Database settings for the intelligence service - databaseType: mysql - databaseSecret: "intelligence-db-secret" # A secret containing the database password - databaseUsername: "intelligence_user" - databaseHost: "mysql" - databasePort: 3306 - databaseUseSSL: true - databaseRequireSSL: false - # Used for constructing database names - legacyDatabaseNames: false - subdomainPrefix: "dev-portal" - # Pod and container security contexts - podSecurityContext: {} - # fsGroup: 1001 - containerSecurityContext: {} - # runAsUser: 1001 - # schedulerName to be applied to pods - schedulerName: "" - # Additional labels to be applied to all resources - additionalLabels: {} - -# Intelligence Server configuration -intelligenceServer: - replicaCount: 1 - kafka: - # Internal Kafka brokers (for intelligence-server's own consumption) - brokers: "kafka:9092" # Internal PLAINTEXT connection - - # Security protocol for intelligence-server's connection - securityProtocol: PLAINTEXT # Intelligence connects internally without SSL - - # Auto-discovery configuration (discovers EXTERNAL IPs for third-party clients) - autoDiscovery: - # When enabled, discovers external LoadBalancer IPs that intelligence-server provides to clients - enabled: true - # Resource requests and limits for the auto-discovery (no longer used) - # resources: {} - - externalAdvertisedBrokers: "" # Populated by auto-discovery - - # Certificates that intelligence-server provides to third-party clients - kafkaCa: - caSecretName: portal-external-secret # Certificates for clients (changed from portal-internal-secret) - passwordSecretName: portal-external-secret - passwordSecretKey: "keypass.txt" - # Keys in the secret for TSSG cert/key (uses apim-ssl certificate with CN=tssg) - # These can be overridden if using custom certificates - caCertKey: "apim-ssl.crt" # Default to apim-ssl, can be overridden - caKeyKey: "apim-ssl.key" # Use .key for intelligence, not .p8.key - # Additional labels for the intelligence deployment - additionalLabels: {} - # Pod affinity, node selector, and tolerations - affinity: {} - nodeSelector: {} - tolerations: [] - # Security contexts for the pod and container - podSecurityContext: {} - containerSecurityContext: {} - # Resource requests and limits - resources: {} - # requests: - # cpu: 200m - # memory: 512Mi - # limits: - # cpu: 500m - # memory: 1Gi - # Service configuration - service: - type: ClusterIP - port: 8282 - sessionAffinity: None - # sessionAffinityConfig: - # clientIP: - # timeoutSeconds: 10800 - externalTrafficPolicy: "" - internalTrafficPolicy: "" - # The host and port for the portal data service - portalDataHost: "portal-data:8080" - -# NOTE: When this chart is used as a subchart, it requires Kafka configuration -# for its initContainer to discover brokers. The parent chart must provide these -# values under the 'apim-intelligence.kafka' key. -# -# To avoid duplication, the parent chart's `values.yaml` can use a YAML alias: -# -# kafka: &kafka_config -# #... kafka subchart values -# -# apim-intelligence: -# kafka: *kafka_config diff --git a/charts/kafka/Chart.yaml b/charts/kafka/Chart.yaml index f1fec618..a5110f58 100644 --- a/charts/kafka/Chart.yaml +++ b/charts/kafka/Chart.yaml @@ -7,5 +7,5 @@ maintainers: - name: Gazza7205 sources: - https://github.com/CAAPIM/apim-charts -version: 1.0.0 +version: 1.0.1 appVersion: "4.0.0" diff --git a/charts/kafka/README.md b/charts/kafka/README.md index 69991281..34853f78 100644 --- a/charts/kafka/README.md +++ b/charts/kafka/README.md @@ -10,12 +10,9 @@ This Kafka subchart provides a production-ready deployment of Apache Kafka 4.0.0 - **KRaft Mode**: Zookeeper-less Kafka using the new KRaft consensus protocol - **Kafka 4.0.0**: Latest stable version with improved performance and features -- **Autodiscovery Support**: External services for each broker enabling autodiscovery by Intelligence service -- **Multi-Listener Support**: Internal, External, and Controller listeners with configurable security -- **SSL/TLS Support**: Full TLS encryption with mTLS client authentication +- **Multi-Listener Support**: Internal and Controller listeners with configurable security - **SASL Authentication**: Support for PLAIN, SCRAM-SHA-256, and SCRAM-SHA-512 - **Flexible Deployment**: StatefulSet with configurable replicas -- **External Access**: LoadBalancer or NodePort services for external connectivity - **RBAC Support**: ServiceAccount for Kubernetes API access ## Quick Start @@ -44,9 +41,6 @@ kafka: limits: cpu: 2000m memory: 4Gi - externalAccess: - enabled: true - serviceType: LoadBalancer ``` ## Configuration @@ -67,38 +61,17 @@ kafka: | `kafka.listeners.internal.enabled` | Enable internal listener | `true` | | `kafka.listeners.internal.port` | Internal listener port | `9092` | | `kafka.listeners.internal.protocol` | Internal listener protocol | `PLAINTEXT` | -| `kafka.listeners.external.enabled` | Enable external listener | `true` | -| `kafka.listeners.external.port` | External listener port | `9094` | -| `kafka.listeners.external.protocol` | External listener protocol | `SSL` | | `kafka.listeners.controller.enabled` | Enable controller listener | `true` | | `kafka.listeners.controller.port` | Controller listener port | `9093` | | `kafka.listeners.controller.protocol` | Controller listener protocol | `PLAINTEXT` | | `kafka.listeners.interBrokerListenerName` | Inter-broker listener name | `INTERNAL` | | **Retention Configuration** | | `kafka.logRetentionHours` | Log retention period in hours | `6` | -| **TLS/SSL Configuration** | -| `kafka.tls.enabled` | Enable TLS/SSL | `false` | -| `kafka.tls.type` | TLS certificate type (PEM or JKS) | `PEM` | -| `kafka.tls.clientAuth` | Client authentication (none, requested, required) | `required` | -| `kafka.tls.secretName` | Secret containing TLS certificates | `""` | -| `kafka.tls.keystoreKeyKey` | Key for keystore in secret | `"keystore.key"` | -| `kafka.tls.truststoreCertKey` | Key for truststore in secret | `"truststore.pem"` | -| `kafka.tls.passwordSecretName` | Secret containing key password | `""` | -| `kafka.tls.passwordSecretKey` | Key for password in secret | `"keypass.txt"` | | **SASL Configuration** | | `kafka.sasl.enabled` | Enable SASL authentication | `false` | | `kafka.sasl.mechanisms` | SASL mechanisms | `PLAIN` | | `kafka.sasl.interBrokerProtocol` | Inter-broker SASL mechanism | `PLAIN` | | `kafka.sasl.jaasConfigPath` | JAAS configuration file path | `/opt/ca/kafka/config/kafka_server_jaas.conf` | -| **External Access** | -| `externalAccess.enabled` | Enable external access | `true` | -| `externalAccess.serviceType` | Service type (LoadBalancer or NodePort) | `LoadBalancer` | -| `externalAccess.port` | External port | `9094` | -| `externalAccess.hostname` | External hostname for advertised listeners | `""` | -| `externalAccess.annotations` | Annotations for external services | `{}` | -| `externalAccess.loadBalancerIPs` | Static LoadBalancer IPs | `[]` | -| `externalAccess.nodePorts` | NodePort values | `[]` | -| `externalAccess.autoAdvertisedListeners` | Auto-generate advertised listeners | `true` | | **Persistence** | | `persistence.storage.kafka` | Storage size per broker | `10Gi` | | **Service Account** | @@ -142,107 +115,28 @@ Each Kafka pod is automatically assigned a node ID based on its StatefulSet ordi ## Listener Configuration -### Three-Listener Architecture +### Two-Listener Architecture -The chart configures three listeners: +The chart configures two listeners: 1. **INTERNAL** (port 9092): For inter-broker and internal client communication -2. **EXTERNAL** (port 9094): For external clients (SSL encrypted) -3. **CONTROLLER** (port 9093): For KRaft controller communication +2. **CONTROLLER** (port 9093): For KRaft controller communication + +External access to Kafka (for tenant gateways) is handled by the `KafkaTcpProxyAssertion` on the Ingress gateway, which proxies Kafka traffic with TLS termination and metadata address rewriting. Kafka itself does not need to be directly exposed outside the cluster. ### Advertised Listeners -Advertised listeners are automatically generated: +Advertised listeners are automatically generated by the init container: **Internal**: `INTERNAL://kafka-{id}.kafka:9092` -**External**: `EXTERNAL://kafka-{id}-external:9094` (or custom hostname) Example for 3 brokers: ``` -kafka-0: INTERNAL://kafka-0.kafka:9092,EXTERNAL://kafka-0-external:9094 -kafka-1: INTERNAL://kafka-1.kafka:9092,EXTERNAL://kafka-1-external:9094 -kafka-2: INTERNAL://kafka-2.kafka:9092,EXTERNAL://kafka-2-external:9094 -``` - -## External Access - -### LoadBalancer (Default) - -Creates individual LoadBalancer services for each broker: - -```yaml -externalAccess: - enabled: true - serviceType: LoadBalancer - port: 9094 - annotations: - cloud.google.com/load-balancer-type: Internal -``` - -Services created: -- `kafka-0-external` → LoadBalancer with external IP -- `kafka-1-external` → LoadBalancer with external IP -- `kafka-2-external` → LoadBalancer with external IP - -### NodePort - -Use NodePort for environments without LoadBalancer support: - -```yaml -externalAccess: - enabled: true - serviceType: NodePort - port: 9094 - nodePorts: - - 30094 - - 30095 - - 30096 -``` - -### Static IPs - -Assign static IPs to LoadBalancers: - -```yaml -externalAccess: - loadBalancerIPs: - - 10.0.0.100 - - 10.0.0.101 - - 10.0.0.102 -``` - -## SSL/TLS Configuration - -### Enable TLS - -```yaml -kafka: - kafka: - tls: - enabled: true - type: PEM - clientAuth: required - secretName: kafka-tls-secret - passwordSecretName: kafka-password-secret -``` - -### Create TLS Secret - -```bash -kubectl create secret generic kafka-tls-secret \ - --from-file=keystore.key=kafka-key.pem \ - --from-file=truststore.pem=ca-cert.pem - -kubectl create secret generic kafka-password-secret \ - --from-literal=keypass.txt=your-password +kafka-0: INTERNAL://kafka-0.kafka:9092,CONTROLLER://kafka-0.kafka:9093 +kafka-1: INTERNAL://kafka-1.kafka:9092,CONTROLLER://kafka-1.kafka:9093 +kafka-2: INTERNAL://kafka-2.kafka:9092,CONTROLLER://kafka-2.kafka:9093 ``` -### TLS Certificate Format - -The Kafka container expects PEM format certificates: -- **keystore.key**: Encrypted private key (PKCS#8 format) -- **truststore.pem**: CA certificate chain - ## SASL Authentication ### Enable SASL PLAIN @@ -280,7 +174,7 @@ Mount it in the StatefulSet by customizing the deployment. ## Production Configuration -### 3-Broker Cluster with TLS and External Access +### 3-Broker Cluster ```yaml kafka: @@ -299,10 +193,6 @@ kafka: enabled: true port: 9092 protocol: PLAINTEXT - external: - enabled: true - port: 9094 - protocol: SSL controller: enabled: true port: 9093 @@ -311,13 +201,6 @@ kafka: logRetentionHours: 168 # 7 days - tls: - enabled: true - type: PEM - clientAuth: required - secretName: kafka-tls-secret - passwordSecretName: kafka-password-secret - resources: requests: cpu: 2000m @@ -340,37 +223,8 @@ kafka: persistence: storage: kafka: 100Gi - - externalAccess: - enabled: true - serviceType: LoadBalancer - port: 9094 - annotations: - cloud.google.com/load-balancer-type: Internal - loadBalancerIPs: - - 10.0.0.100 - - 10.0.0.101 - - 10.0.0.102 -``` - -## Autodiscovery Integration - -The Intelligence service uses an init container to discover Kafka broker addresses: - -```yaml -apim-intelligence: - kafka: *kafka_config # Reference Kafka config - intelligenceServer: - kafka: - autoDiscovery: - enabled: true ``` -The autodiscovery looks for services named: -- `-kafka-0-external` -- `-kafka-1-external` -- `-kafka-2-external` - ## Migration from Zookeeper Mode If migrating from Zookeeper-based Kafka: @@ -398,27 +252,6 @@ Common issues: - Node ID conflicts - Storage not formatted -### External Access Not Working - -Verify external services: -```bash -kubectl get svc | grep kafka.*external -kubectl describe svc kafka-0-external -``` - -Check advertised listeners: -```bash -kubectl exec kafka-0 -- cat /shared/node-id.env -``` - -### TLS Connection Failures - -Verify certificates: -```bash -kubectl get secret kafka-tls-secret -o yaml -kubectl exec kafka-0 -- ls -la /opt/ca/kafka/config/certs/ -``` - ### Controller Election Issues Check controller logs: @@ -449,94 +282,30 @@ For production deployments, consider adding: - **1.1.0**: KRaft mode support, Kafka 4.0.0, multi-listener architecture - **1.0.0**: Initial release with Zookeeper mode -## References - -- [Apache Kafka Documentation](https://kafka.apache.org/documentation/) -- [KRaft Mode Documentation](https://kafka.apache.org/documentation/#kraft) -- [Kafka 4.0.0 Release Notes](https://kafka.apache.org/40/documentation.html#upgrade) - -## Certificate Configuration (New in v1.0.0) - -The Kafka subchart now supports **three different approaches** for certificate management to ensure compatibility with different deployment scenarios: - -### Option 1: Kubernetes Secrets (Recommended) - -Use native Kubernetes secrets for certificate management: - -```yaml -kafka: - tls: - enabled: true - type: PEM - clientAuth: required - secretName: kafka-tls-secret - keystoreKeyKey: "keystore.key" - truststoreCertKey: "truststore.pem" -``` - -Create the secret: -```bash -kubectl create secret generic kafka-tls-secret \ - --from-file=keystore.key=./certs/kafka-key.pem \ - --from-file=truststore.pem=./certs/ca-cert.pem -``` +## Additional Configuration -### Option 2: Inline Certificates (portal-dist Compatible) +### Portal Subdomain -Pass certificates directly as environment variables (compatible with Docker Swarm portal-dist): - -```yaml -kafka: - tls: - enabled: true - truststoreCertContent: | - -----BEGIN CERTIFICATE----- - MIIDXTCCAkWgAwIBAgIJAKL... - -----END CERTIFICATE----- - keystoreKeyContent: | - -----BEGIN ENCRYPTED PRIVATE KEY----- - MIIFHDBOBgkqhkiG9w0BBQ0w... - -----END ENCRYPTED PRIVATE KEY----- -``` - -### Option 3: CA Certificates (analytics_util Compatible) - -Use CA certificate variables (compatible with analytics_util reference implementation): - -```yaml -kafka: - tls: - enabled: true - caKey: "{{ auth.TSSG_CA_KEY }}" - caCertificate: "{{ auth.TSSG_SSL_CERT }}" - caTruststoreCertificate: "{{ auth.TSSG_TRUSTSTORE_CERT }}" -``` - -### Portal Subdomain Configuration - -For advertised listeners that include the portal subdomain: +For deployments that require the portal subdomain: ```yaml kafka: global: subdomainPrefix: "dev-portal" - externalAccess: - enabled: true - hostname: "apim-kafka.dev-portal" ``` -This sets the `APIM_PORTAL_SUBDOMAIN` environment variable and configures advertised listeners appropriately. +This sets the `APIM_PORTAL_SUBDOMAIN` environment variable. ### Custom Advertised Listeners -For full control over advertised listeners (portal-dist style): +For full control over advertised listeners: ```yaml kafka: - advertisedListeners: "INTERNAL://kafka:9092,EXTERNAL://apim-kafka.dev-portal:9094" + advertisedListeners: "INTERNAL://kafka:9092" ``` -## Portal-dist Compatibility +### Portal-dist Compatibility To match portal-dist Docker Swarm deployment configuration: @@ -553,32 +322,17 @@ kafka: nodeId: "0" controllerQuorumVoters: "0@localhost:9093" - advertisedListeners: "INTERNAL://kafka:9092,EXTERNAL://apim-kafka.dev-portal:9094" + advertisedListeners: "INTERNAL://kafka:9092" logRetentionHours: 6 - - tls: - enabled: true - type: PEM - clientAuth: required - truststoreCertContent: "{{ auth.TSSG_TRUSTSTORE_CERT }}" - keystoreKeyContent: "{{ auth.TSSG_CA_P8_KEY }}" - - sasl: - enabled: true - mechanisms: PLAIN - interBrokerProtocol: PLAIN - - externalAccess: - enabled: true - serviceType: LoadBalancer - port: 9094 - hostname: "apim-kafka.dev-portal" ``` This configuration produces the same environment variables as portal-dist `env-kafka` file. -## Additional Documentation +## References +- [Apache Kafka Documentation](https://kafka.apache.org/documentation/) +- [KRaft Mode Documentation](https://kafka.apache.org/documentation/#kraft) +- [Kafka 4.0.0 Release Notes](https://kafka.apache.org/40/documentation.html#upgrade) - **[CONFIGURATION_GUIDE.md](CONFIGURATION_GUIDE.md)** - Detailed configuration guide with examples - **[QUICK_START.md](QUICK_START.md)** - Quick start guide - **[../../KAFKA_ENV_COMPARISON.md](../../KAFKA_ENV_COMPARISON.md)** - Environment variables comparison diff --git a/charts/kafka/kafka-1.0.1.tgz b/charts/kafka/kafka-1.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5aa4718b0089f7de11ffeba9403867c016bacb03 GIT binary patch literal 12603 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

8GL(SVddFa$KhB; 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 /shared/node-id.env echo "Wrote KAFKA_CFG_NODE_ID to /shared/node-id.env" - {{- if .Values.kafka.tls.enabled }} - # Note: TSSG certificates will be loaded by main container from /opt/ca/certs/ - echo "=== TSSG certificates (apim-ssl) available at /opt/ca/certs/ ===" - {{- end }} - - {{- if and .Values.externalAccess.enabled .Values.externalAccess.autoAdvertisedListeners }} - # External access is enabled (typically for intelligence feature) - # This block discovers LoadBalancer/NodePort IPs for external clients - echo "Building advertised listeners with external access..." - INTERNAL_LISTENER="INTERNAL://{{ include "kafka.statefulsetName" . }}-${NODE_ID}.{{ include "kafka.fullname" . }}:{{ .Values.kafka.listeners.internal.port }}" - echo "Internal listener: $INTERNAL_LISTENER" - - CONTROLLER_LISTENER="CONTROLLER://{{ include "kafka.statefulsetName" . }}-${NODE_ID}.{{ include "kafka.fullname" . }}:{{ .Values.kafka.listeners.controller.port }}" - echo "Controller listener: $CONTROLLER_LISTENER" - - {{- if .Values.externalAccess.hostname }} - # Use provided hostname for external listener - EXTERNAL_LISTENER="EXTERNAL://{{ .Values.externalAccess.hostname }}:{{ .Values.externalAccess.port }}" - echo "External listener (hostname): $EXTERNAL_LISTENER" - {{- else }} - # Discover external IP/hostname - EXTERNAL_SVC="{{ include "kafka.statefulsetName" . }}-${NODE_ID}-external" - echo "Discovering external address for service: $EXTERNAL_SVC" - - # Get the service type - SERVICE_TYPE=$(kubectl get svc "$EXTERNAL_SVC" -n "{{ .Release.Namespace }}" -o jsonpath='{.spec.type}' 2>/dev/null || echo "") - echo "Service type: $SERVICE_TYPE" - - EXTERNAL_ADDRESS="" - EXTERNAL_PORT="{{ .Values.externalAccess.port }}" - - if [ "$SERVICE_TYPE" = "LoadBalancer" ]; then - # Configurable timeout and retry settings - TIMEOUT_SECONDS={{ .Values.externalAccess.discovery.timeoutSeconds | default 120 }} - SLEEP_TIME={{ .Values.externalAccess.discovery.retryIntervalSeconds | default 5 }} - RETRIES=$((TIMEOUT_SECONDS / SLEEP_TIME)) - FAIL_ON_ERROR={{ .Values.externalAccess.discovery.failOnDiscoveryFailure | default true }} - - echo "Waiting for LoadBalancer IP (max ${TIMEOUT_SECONDS} seconds)..." - # Wait for LoadBalancer IP with configurable timeout and better error handling - - for i in $(seq 1 $RETRIES); do - # Try to get IP first, then hostname - LB_IP=$(kubectl get svc "$EXTERNAL_SVC" -n "{{ .Release.Namespace }}" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>&1) - KUBECTL_EXIT_CODE=$? - - # If kubectl command failed, log the error - if [ $KUBECTL_EXIT_CODE -ne 0 ]; then - echo "ERROR: kubectl command failed (exit code: $KUBECTL_EXIT_CODE)" - echo "Error details: $LB_IP" - echo "Checking RBAC permissions and ServiceAccount..." - kubectl auth can-i get services --as=system:serviceaccount:{{ .Release.Namespace }}:{{ include "kafka.serviceAccountName" . }} -n {{ .Release.Namespace }} 2>&1 || echo "RBAC check failed" - fi - - # If IP is empty, try hostname - if [ -z "$LB_IP" ]; then - LB_IP=$(kubectl get svc "$EXTERNAL_SVC" -n "{{ .Release.Namespace }}" -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>&1) - fi - - if [ -n "$LB_IP" ] && [ "$LB_IP" != "null" ]; then - EXTERNAL_ADDRESS="$LB_IP" - echo "✓ Successfully discovered LoadBalancer address: ${EXTERNAL_ADDRESS}" - break - fi - - echo "Attempt $i/$RETRIES: LoadBalancer address not ready yet, waiting ${SLEEP_TIME}s..." - sleep $SLEEP_TIME - done - - if [ -z "$EXTERNAL_ADDRESS" ]; then - echo "==========================================" - echo "ERROR: LoadBalancer IP not available after ${TIMEOUT_SECONDS} seconds" - echo "Service: $EXTERNAL_SVC" - echo "Namespace: {{ .Release.Namespace }}" - echo "==========================================" - echo "" - echo "Troubleshooting steps:" - echo "1. Check if LoadBalancer service is created: kubectl get svc $EXTERNAL_SVC -n {{ .Release.Namespace }}" - echo "2. Check LoadBalancer status: kubectl describe svc $EXTERNAL_SVC -n {{ .Release.Namespace }}" - echo "3. Check cloud provider LoadBalancer provisioning" - echo "4. Verify RBAC permissions for ServiceAccount: {{ include "kafka.serviceAccountName" . }}" - echo "5. Check if ServiceAccount token is mounted: ls -la /var/run/secrets/kubernetes.io/serviceaccount/" - echo "==========================================" - - if [ "$FAIL_ON_ERROR" = "true" ]; then - echo "FATAL: failOnDiscoveryFailure is enabled. Pod will fail and Kubernetes will retry." - echo "This prevents Kafka from starting with incorrect advertised listeners." - exit 1 - else - echo "WARNING: Using service hostname as fallback (NOT RECOMMENDED)" - echo "This may cause 'Error connecting to node' issues for external clients." - EXTERNAL_ADDRESS="${EXTERNAL_SVC}" - fi - fi - elif [ "$SERVICE_TYPE" = "NodePort" ]; then - # For NodePort, try to get node IP and NodePort - echo "Discovering NodePort configuration..." - NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="ExternalIP")].address}' 2>&1) - if [ -z "$NODE_IP" ]; then - NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>&1) - fi - NODE_PORT=$(kubectl get svc "$EXTERNAL_SVC" -n "{{ .Release.Namespace }}" -o jsonpath='{.spec.ports[0].nodePort}' 2>&1) - - if [ -n "$NODE_IP" ] && [ -n "$NODE_PORT" ] && [ "$NODE_IP" != "null" ] && [ "$NODE_PORT" != "null" ]; then - EXTERNAL_ADDRESS="${NODE_IP}" - EXTERNAL_PORT="${NODE_PORT}" - echo "✓ Successfully discovered NodePort: ${EXTERNAL_ADDRESS}:${EXTERNAL_PORT}" - else - echo "==========================================" - echo "FATAL ERROR: Could not discover NodePort configuration" - echo "Service: $EXTERNAL_SVC" - echo "Node IP: $NODE_IP" - echo "Node Port: $NODE_PORT" - echo "==========================================" - echo "This is a critical error. The pod will fail and Kubernetes will retry." - exit 1 - fi - else - # Fallback to service name - echo "Using service name as fallback" - EXTERNAL_ADDRESS="${EXTERNAL_SVC}" - fi - - EXTERNAL_LISTENER="EXTERNAL://${EXTERNAL_ADDRESS}:${EXTERNAL_PORT}" - echo "External listener: $EXTERNAL_LISTENER" - {{- end }} - - ADVERTISED_LISTENERS="${INTERNAL_LISTENER},${CONTROLLER_LISTENER},${EXTERNAL_LISTENER}" - echo "KAFKA_CFG_ADVERTISED_LISTENERS=$ADVERTISED_LISTENERS" >> /shared/node-id.env - echo "=== Final advertised listeners: $ADVERTISED_LISTENERS ===" - {{- else }} - # External access is disabled (intelligence not enabled) - # No LoadBalancer discovery needed - using internal listeners only - # Pod will start successfully without external dependencies - echo "Building advertised listeners (internal only)..." + echo "Building advertised listeners..." INTERNAL_LISTENER="INTERNAL://{{ include "kafka.statefulsetName" . }}-${NODE_ID}.{{ include "kafka.fullname" . }}:{{ .Values.kafka.listeners.internal.port }}" echo "Internal listener: $INTERNAL_LISTENER" @@ -276,7 +125,6 @@ spec: echo "Controller listener: $CONTROLLER_LISTENER" echo "KAFKA_CFG_ADVERTISED_LISTENERS=$INTERNAL_LISTENER,$CONTROLLER_LISTENER" >> /shared/node-id.env - {{- end }} echo "=== Configuration written to /shared/node-id.env ===" cat /shared/node-id.env @@ -329,27 +177,6 @@ spec: echo "Final KAFKA_CFG_ADVERTISED_LISTENERS: $KAFKA_CFG_ADVERTISED_LISTENERS" fi - {{- if .Values.kafka.tls.enabled }} - # Load TSSG certificates (apim-ssl) from mounted volume - # Note: Volume mount maps secret keys to apim-ssl.p8.key and apim-ssl.crt paths - echo "=== Loading TSSG certificates for Kafka SSL ===" - if [ -f /opt/ca/certs/apim-ssl.p8.key ]; then - export KAFKA_CFG_SSL_KEYSTORE_KEY=$(cat /opt/ca/certs/apim-ssl.p8.key) - echo "Loaded TSSG keystore key (PKCS#8)" - else - echo "ERROR: TSSG keystore key not found at /opt/ca/certs/apim-ssl.p8.key" - exit 1 - fi - - if [ -f /opt/ca/certs/apim-ssl.crt ]; then - export KAFKA_CFG_SSL_TRUSTSTORE_CERTIFICATES=$(cat /opt/ca/certs/apim-ssl.crt) - echo "Loaded TSSG truststore certificate" - else - echo "ERROR: TSSG truststore certificate not found at /opt/ca/certs/apim-ssl.crt" - exit 1 - fi - {{- end }} - # Check if KAFKA_CLUSTER_ID is set and format storage if needed if [ -n "$KAFKA_CLUSTER_ID" ]; then echo "=== Kafka KRaft Storage Formatting ===" @@ -406,29 +233,6 @@ spec: valueFrom: fieldRef: fieldPath: metadata.name - - name: INTELLIGENCE_ENABLED - value: {{ .Values.externalAccess.enabled | quote }} - {{- if .Values.kafka.tls.enabled }} - {{- if .Values.kafka.tls.secretName }} - - name: KAFKA_CFG_SSL_KEYSTORE_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.kafka.tls.secretName }} - key: {{ .Values.kafka.tls.keystoreKeyKey }} - - name: KAFKA_CFG_SSL_TRUSTSTORE_CERTIFICATES - valueFrom: - secretKeyRef: - name: {{ .Values.kafka.tls.secretName }} - key: {{ .Values.kafka.tls.truststoreCertKey }} - {{- end }} - {{- if .Values.kafka.tls.passwordSecretName }} - - name: KAFKA_CFG_SSL_KEY_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.kafka.tls.passwordSecretName }} - key: {{ .Values.kafka.tls.passwordSecretKey }} - {{- end }} - {{- end }} envFrom: - configMapRef: name: {{ include "kafka.fullname" . }}-config @@ -454,10 +258,6 @@ spec: ports: - containerPort: {{ .Values.kafka.listeners.internal.port }} name: kafka - {{- if .Values.externalAccess.enabled }} - - containerPort: {{ .Values.kafka.listeners.external.port }} - name: external - {{- end }} {{- if and .Values.kafka.kraft.enabled .Values.kafka.listeners.controller.enabled }} - containerPort: {{ .Values.kafka.listeners.controller.port }} name: controller @@ -470,11 +270,6 @@ spec: - name: shared-config mountPath: /shared {{- end }} - {{- if .Values.kafka.tls.enabled }} - - name: internal-certs - mountPath: /opt/ca/certs - readOnly: true - {{- end }} {{- if .Values.global.pullSecret }} imagePullSecrets: - name: "{{ .Values.global.pullSecret }}" @@ -484,16 +279,6 @@ spec: - name: shared-config emptyDir: {} {{- end }} - {{- if .Values.kafka.tls.enabled }} - - name: internal-certs - secret: - secretName: {{ .Values.kafka.tls.internalSecretName | default "portal-internal-secret" }} - items: - - key: {{ .Values.kafka.tls.tssgKeyKey }} - path: apim-ssl.p8.key - - key: {{ .Values.kafka.tls.tssgCertKey }} - path: apim-ssl.crt - {{- end }} restartPolicy: Always terminationGracePeriodSeconds: 60 volumeClaimTemplates: diff --git a/charts/kafka/templates/pre-upgrade-statefulset-cleanup.yaml b/charts/kafka/templates/pre-upgrade-statefulset-cleanup.yaml index da1a2d3e..fa1f5c86 100644 --- a/charts/kafka/templates/pre-upgrade-statefulset-cleanup.yaml +++ b/charts/kafka/templates/pre-upgrade-statefulset-cleanup.yaml @@ -1,3 +1,4 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. {{- if .Values.kafka.kraft.enabled }} # # Pre-Upgrade StatefulSet Cleanup Job @@ -107,19 +108,12 @@ spec: {{- end }} containers: - name: sts-cleanup - {{- if .Values.image }} - {{- if .Values.image.autoDiscovery }} - # Use parent chart's autoDiscovery image (when kafka is a subchart) + {{- if and .Values.image .Values.image.autoDiscovery }} image: "{{ .Values.global.portalRepository }}{{ .Values.image.autoDiscovery }}" {{- else }} - # Use local discoveryImage configuration - image: "{{ .Values.global.portalRepository }}{{ .Values.externalAccess.discoveryImage.repository }}:{{ .Values.externalAccess.discoveryImage.tag }}" + image: "{{ .Values.global.portalRepository }}{{ .Values.kubectlImage.repository }}:{{ .Values.kubectlImage.tag }}" {{- end }} - {{- else }} - # Use local discoveryImage configuration (standalone kafka chart) - image: "{{ .Values.global.portalRepository }}{{ .Values.externalAccess.discoveryImage.repository }}:{{ .Values.externalAccess.discoveryImage.tag }}" - {{- end }} - imagePullPolicy: {{ .Values.externalAccess.discoveryImage.pullPolicy | default "IfNotPresent" }} + imagePullPolicy: {{ .Values.kubectlImage.pullPolicy | default "IfNotPresent" }} command: - /bin/sh - -c diff --git a/charts/kafka/values.yaml b/charts/kafka/values.yaml index 22fa236b..bf197ab5 100644 --- a/charts/kafka/values.yaml +++ b/charts/kafka/values.yaml @@ -83,11 +83,6 @@ kafka: enabled: true port: 9092 protocol: PLAINTEXT - # External listener for clients outside the cluster - external: - enabled: true - port: 9094 - protocol: SSL # Controller listener for KRaft mode controller: enabled: true @@ -97,13 +92,14 @@ kafka: interBrokerListenerName: INTERNAL # Advertised Listeners Configuration # Custom advertised listeners (portal-dist style) - # Format: "INTERNAL://kafka:9092,EXTERNAL://apim-kafka.subdomain:9094" - # Leave empty to auto-generate based on externalAccess settings + # Format: "INTERNAL://kafka:9092" + # Leave empty to auto-generate advertisedListeners: "" # Log retention configuration logRetentionHours: 6 +<<<<<<< HEAD # TLS/SSL Configuration tls: # Enable TLS/SSL (required when external listener uses SSL protocol) @@ -148,6 +144,8 @@ kafka: caKey: "" caCertificate: "" caTruststoreCertificate: "" +======= +>>>>>>> 44b9d09 (remove kafka tls) # SASL Authentication Configuration sasl: # Enable SASL authentication @@ -181,44 +179,9 @@ serviceAccount: # Automount service account token automountServiceAccountToken: true -# External access configuration for autodiscovery -externalAccess: - # External access should be enabled only when Intelligence is enabled - # Analytics (Druid) uses internal Kafka service only - enabled: false - # Service type for external access (LoadBalancer or NodePort) - serviceType: LoadBalancer - # Port for external access (should match kafka.listeners.external.port) - port: 9094 - # Hostname/domain for external access (used in advertised listeners) - # Format: apim-kafka.{subdomain} or specific hostname - hostname: "" - # Annotations for external services - annotations: {} - # LoadBalancer IPs (optional, for static IPs) - loadBalancerIPs: [] - # NodePort values (optional, for NodePort service type) - nodePorts: [] - # Auto-generate advertised listeners based on service IPs - autoAdvertisedListeners: true - # Discovery configuration for LoadBalancer/NodePort IP detection - # NOTE: This only applies when externalAccess.enabled is true - # When intelligence is disabled, Kafka uses internal-only listeners and these settings are ignored - discovery: - # Maximum time to wait for LoadBalancer IP (in seconds) - # Default: 120 seconds (24 retries * 5 seconds) - timeoutSeconds: 120 - # Retry interval (in seconds) - retryIntervalSeconds: 5 - # Fail fast: If true, pod will fail when LoadBalancer IP cannot be discovered - # This prevents Kafka from starting with incorrect advertised listeners - # If false, falls back to service hostname (NOT RECOMMENDED - causes connection issues) - # IMPORTANT: Only affects behavior when externalAccess.enabled=true - failOnDiscoveryFailure: true - # Discovery image for LoadBalancer IP detection (kubectl) - # Note: When used as a subchart, image.autoDiscovery should be provided by parent chart - # When used standalone, provide these values during deployment - discoveryImage: - repository: "tls-automator" - tag: "latest" # Should be overridden with specific version - pullPolicy: IfNotPresent +# kubectl image for pre-upgrade cleanup jobs +# Note: When used as a subchart, image.autoDiscovery should be provided by parent chart +kubectlImage: + repository: "tls-automator" + tag: "latest" + pullPolicy: IfNotPresent diff --git a/charts/portal/Chart.lock b/charts/portal/Chart.lock index 405c8f1a..1bd46c16 100644 --- a/charts/portal/Chart.lock +++ b/charts/portal/Chart.lock @@ -5,17 +5,14 @@ dependencies: - name: rabbitmq repository: "" version: 12.0.3 -- name: apim-intelligence - repository: file://../intelligence - version: 1.0.21 - name: seaweedfs repository: file://../seaweedfs version: 1.0.4 - name: kafka repository: file://../kafka - version: 1.0.0 + version: 1.0.1 - name: ingress-nginx repository: https://kubernetes.github.io/ingress-nginx/ version: 4.12.1 -digest: sha256:855a759dfaf53f78e48f0e4f3ea66bf072aeb997ab1e9a547ac6b9f58f6beb25 -generated: "2026-02-27T20:46:54.2953154+05:30" +digest: sha256:a3d25593eb6299015d8e8b99970bff8858a3d8cb71bb4bae34829995e9d20ecf +generated: "2026-03-13T11:18:42.831349+05:30" diff --git a/charts/portal/Chart.yaml b/charts/portal/Chart.yaml index 7ceac8fe..45c2dde0 100644 --- a/charts/portal/Chart.yaml +++ b/charts/portal/Chart.yaml @@ -17,16 +17,12 @@ dependencies: - name: rabbitmq version: 12.0.3 condition: rabbitmq.enabled -- name: apim-intelligence - version: ^1.0.0 - condition: portal.intelligence.enabled - repository: "file://../intelligence" - name: seaweedfs version: ^1.0.0 condition: portal.analytics.enabled repository: "file://../seaweedfs" - name: kafka - version: ^1.0.0 + version: ^1.0.1 condition: kafka.enabled repository: "file://../kafka" - name: ingress-nginx diff --git a/charts/portal/README.md b/charts/portal/README.md index 11de5db4..c970a3c6 100644 --- a/charts/portal/README.md +++ b/charts/portal/README.md @@ -825,7 +825,7 @@ Portal Analytics For Production, use an external MySQL Server. ## Intelligence (APIM Intelligence) -The Intelligence subchart provides advanced analytics and third-party agent integration capabilities. +The Intelligence server provides advanced analytics and third-party agent integration capabilities. **Important Dependencies:** - When `portal.intelligence.enabled: true`, the following subcharts are automatically deployed: @@ -834,10 +834,8 @@ The Intelligence subchart provides advanced analytics and third-party agent inte | Parameter | Description | Default | | --- | --- | --- | | `portal.intelligence.enabled` | Enable Intelligence service | `false` | -| `apim-intelligence.intelligenceServer.replicaCount` | Number of intelligence server replicas | `1` | -| `apim-intelligence.intelligenceServer.kafka.autoDiscovery.enabled` | Enable Kafka broker auto-discovery | `true` | - -For detailed Intelligence configuration, see the [Intelligence Chart README](../intelligence/README.md). +| `intelligence.replicaCount` | Number of intelligence server replicas | `1` | +| `intelligence.kafka.kafkaCa.caSecretName` | Secret containing the CA certificate for Kafka mTLS | `portal-external-secret` | ## SeaweedFS The SeaweedFS subchart provides S3-compatible object storage for analytics data. @@ -872,8 +870,6 @@ The Kafka subchart provides Apache Kafka 4.0.0 in KRaft mode (Zookeeper-less) fo | `kafka.enabled` | Enable Kafka subchart | `false` | | `kafka.kafka.replicaCount` | Number of Kafka broker replicas (3+ recommended for production) | `1` | | `kafka.kafka.kraft.enabled` | Enable KRaft mode (Zookeeper-less) | `true` | -| `kafka.externalAccess.enabled` | Enable external access to Kafka brokers | `true` | -| `kafka.externalAccess.serviceType` | Service type for external access | `LoadBalancer` | For detailed Kafka configuration, see the [Kafka Chart README](../kafka/README.md). diff --git a/charts/portal/charts/apim-intelligence-1.0.21.tgz b/charts/portal/charts/apim-intelligence-1.0.21.tgz deleted file mode 100644 index b0bb5cb9c145330b57a97126501c9efc026079c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10201 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 T_Wb_<00960iMl4V0M-Bi)6nDU diff --git a/charts/portal/templates/_helpers.tpl b/charts/portal/templates/_helpers.tpl index 75f80f28..1b13d4cf 100644 --- a/charts/portal/templates/_helpers.tpl +++ b/charts/portal/templates/_helpers.tpl @@ -419,6 +419,20 @@ Create Image Pull Secret {{- .Values.ingress.gatewayAPI.tlsRouteApiVersion -}} {{- end -}} +{{/* +Generate Kafka TCP Proxy ingress hostname based on configurations. +Used when intelligence is enabled. +*/}} +{{- define "kafka-proxy-host" -}} + {{- if .Values.global.legacyHostnames }} + {{- printf "kafka-proxy.%s" .Values.portal.domain -}} + {{- else if .Values.global.saas }} + {{- printf "kafka-proxy-%s.%s" .Values.global.subdomainPrefix .Values.portal.domain -}} + {{- else }} + {{- printf "%s-kafka-proxy.%s" .Values.global.subdomainPrefix .Values.portal.domain -}} + {{- end }} +{{- end -}} + {{/* Generate default parentRefs for routes when none are provided. Produces a list with a single parentRef pointing to the chart's Gateway. @@ -427,3 +441,64 @@ Create Image Pull Secret - name: {{ include "portal.gatewayAPI.gatewayName" . }} namespace: {{ include "portal.gatewayAPI.gatewayNamespace" . }} {{- end -}} + +{{/* + ============================================================ + Intelligence Server Helpers + ============================================================ +*/}} + +{{/* +Get "intelligence" database name +*/}} +{{- define "intelligence-db-name" -}} + {{ if .Values.global.legacyDatabaseNames }} + {{- print "intelligence" }} + {{- else }} + {{- $f:= .Values.global.subdomainPrefix -}} + {{ if empty $f }} + {{- fail "Please define subdomainPrefix in values.yaml" }} + {{- else }} + {{- printf "%s_%s" $f "intelligence" | replace "-" "_" -}} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Set the service account name for the Intelligence Server +*/}} +{{- define "intelligence.serviceAccountName" -}} +{{- if .Values.global.serviceAccountName }} + {{ default "default" .Values.global.serviceAccountName }} +{{- else }} +{{- if .Values.intelligence.serviceAccount.create -}} + {{ default "intelligence-server" .Values.intelligence.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.intelligence.serviceAccount.name }} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Get Kafka broker address for intelligence server. +Uses the kafka subchart's fullnameOverride and internal listener port. +*/}} +{{- define "intelligence.kafkaBrokers" -}} + {{- $kafkaName := default "kafka" .Values.kafka.fullnameOverride -}} + {{- if and .Values.kafka.kafka .Values.kafka.kafka.listeners }} + {{- printf "%s:%g" $kafkaName .Values.kafka.kafka.listeners.internal.port -}} + {{- else }} + {{- printf "%s:9092" $kafkaName -}} + {{- end }} +{{- end -}} + +{{/* +Generate Kafka proxy bootstrap address for intelligence server. +Format: {kafka-proxy-host}:{listenPort} +Reuses the kafka-proxy-host helper and the kafkaProxy.listenPort value. +*/}} +{{- define "intelligence.kafkaProxyBootstrap" -}} + {{- $host := include "kafka-proxy-host" . -}} + {{- $port := int (.Values.portal.intelligence.kafkaProxy.listenPort | default 9192) -}} + {{- printf "%s:%d" $host $port -}} +{{- end -}} diff --git a/charts/portal/templates/apim/apim-config.yaml b/charts/portal/templates/apim/apim-config.yaml index 0ccaf6e7..6afebc73 100644 --- a/charts/portal/templates/apim/apim-config.yaml +++ b/charts/portal/templates/apim/apim-config.yaml @@ -1,3 +1,5 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. +# AI assistance has been used to generate some or all contents of this file. That includes, but is not limited to, new code, modifying existing code, stylistic edits. apiVersion: v1 kind: ConfigMap metadata: @@ -29,10 +31,12 @@ data: TSSG_PUBLIC_HOST: {{ include "tssg-public-host" . | quote }} TSSG_PUBLIC_PORT: {{ .Values.portal.otk.port | quote }} {{- if .Values.portal.intelligence.enabled }} - # Intelligence Feature Flag INTELLIGENCE_ENABLED: "true" + KAFKA_PROXY_TARGET_SERVERS: {{ default "kafka:9092" .Values.portal.intelligence.kafkaProxy.targetBootstrapServers | quote }} + KAFKA_PROXY_LISTEN_PORT: {{ default 9192 .Values.portal.intelligence.kafkaProxy.listenPort | quote }} + KAFKA_PROXY_BROKER_START_PORT: {{ default 9193 .Values.portal.intelligence.kafkaProxy.brokerStartPort | quote }} + KAFKA_PROXY_ADVERTISED_HOST: {{ default (include "tssg-public-host" .) .Values.portal.intelligence.kafkaProxy.advertisedHost | quote }} {{- else }} - # Intelligence Feature Flag INTELLIGENCE_ENABLED: "false" {{- end }} {{ if .Values.apim.additionalEnv }} diff --git a/charts/portal/templates/apim/apim-service.yaml b/charts/portal/templates/apim/apim-service.yaml index d91fc99d..74c3e5df 100644 --- a/charts/portal/templates/apim/apim-service.yaml +++ b/charts/portal/templates/apim/apim-service.yaml @@ -1,3 +1,4 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. apiVersion: v1 kind: Service metadata: @@ -41,6 +42,25 @@ spec: targetPort: 9448 protocol: TCP name: apim-sso +{{- if .Values.portal.intelligence.enabled }} + - port: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} + targetPort: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} + protocol: TCP + name: kafka-proxy-bootstrap +{{- $brokerStartPort := int (.Values.portal.intelligence.kafkaProxy.brokerStartPort | default 9193) }} +{{- $replicaCount := int (.Values.kafka.kafka.replicaCount | default 1) }} +{{- $ordinalStart := 0 }} +{{- if and .Values.kafka.kafka.ordinals (hasKey .Values.kafka.kafka.ordinals "start") }} +{{- $ordinalStart = int .Values.kafka.kafka.ordinals.start }} +{{- end }} +{{- range $i := until $replicaCount }} +{{- $brokerPort := add $brokerStartPort (add $ordinalStart $i) }} + - port: {{ $brokerPort }} + targetPort: {{ $brokerPort }} + protocol: TCP + name: kafka-proxy-broker-{{ add $ordinalStart $i }} +{{- end }} +{{- end }} selector: app: apim type: ClusterIP diff --git a/charts/portal/templates/gateway-api/gateway.yaml b/charts/portal/templates/gateway-api/gateway.yaml index 4d4ee813..4e7f42f6 100644 --- a/charts/portal/templates/gateway-api/gateway.yaml +++ b/charts/portal/templates/gateway-api/gateway.yaml @@ -41,6 +41,10 @@ spec: {{- $hostnames = append $hostnames (include "pssg-sync-host" .) }} {{- $hostnames = append $hostnames (include "pssg-sso-host" .) }} {{- $hostnames = append $hostnames (include "broker-host" .) }} + {{/* Kafka proxy route */}} + {{- if .Values.portal.intelligence.enabled }} + {{- $hostnames = append $hostnames (include "kafka-proxy-host" .) }} + {{- end }} {{/* Dynamic tenant routes */}} {{- range .Values.ingress.tenantIds }} {{- $hostnames = append $hostnames (printf "%s.%s" . $.Values.portal.domain) }} diff --git a/charts/portal/templates/gateway-api/tlsroute.yaml b/charts/portal/templates/gateway-api/tlsroute.yaml index cf05311f..eaebdf53 100644 --- a/charts/portal/templates/gateway-api/tlsroute.yaml +++ b/charts/portal/templates/gateway-api/tlsroute.yaml @@ -232,6 +232,40 @@ spec: - backendRefs: - name: apim port: 1885 +{{- if .Values.portal.intelligence.enabled }} +--- +apiVersion: {{ include "portal.gatewayAPI.tlsRouteApiVersion" $root }} +kind: TLSRoute +metadata: + name: portal-tlsroute-kafka-proxy + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "portal.name" . }} + chart: {{ template "portal.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + {{- range $key, $val := .Values.global.additionalLabels }} + {{ $key }}: "{{ $val }}" + {{- end }} + {{- range $key, $val := .Values.ingress.gatewayAPI.labels }} + {{ $key }}: "{{ $val }}" + {{- end }} + {{- if $annotations }} + annotations: + {{- range $key, $val := $annotations }} + {{ $key }}: "{{ $val }}" + {{- end }} + {{- end }} +spec: + parentRefs: + {{- include "portal.gatewayAPI.defaultParentRefs" . | nindent 4 }} + hostnames: + - {{ include "kafka-proxy-host" . | quote }} + rules: + - backendRefs: + - name: apim + port: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} +{{- end }} {{/* ============================================================ Dynamic TLSRoutes - generated from ingress.tenantIds diff --git a/charts/portal/templates/ingress/contour-httpproxy.yaml b/charts/portal/templates/ingress/contour-httpproxy.yaml index a06f7408..c0cd20bd 100644 --- a/charts/portal/templates/ingress/contour-httpproxy.yaml +++ b/charts/portal/templates/ingress/contour-httpproxy.yaml @@ -176,6 +176,34 @@ spec: port: 1885 --- +{{- if .Values.portal.intelligence.enabled }} +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: portal-kafka-proxy-httpproxy + labels: + app: {{ template "portal.name" . }} + chart: {{ template "portal.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + {{- range $key, $val := .Values.global.additionalLabels }} + {{ $key }}: "{{ $val }}" + {{- end }} + {{- range $key, $val := .Values.ingress.additionalLabels }} + {{ $key }}: "{{ $val }}" + {{- end }} +spec: + virtualhost: + fqdn: {{ include "kafka-proxy-host" . | quote }} + tls: + passthrough: true + tcpproxy: + services: + - name: apim + port: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} +{{- end }} + {{- range $i,$tenantId := $.Values.ingress.tenantIds }} --- apiVersion: projectcontour.io/v1 diff --git a/charts/portal/templates/ingress/ingress.yaml b/charts/portal/templates/ingress/ingress.yaml index 547e283c..f34b74ff 100644 --- a/charts/portal/templates/ingress/ingress.yaml +++ b/charts/portal/templates/ingress/ingress.yaml @@ -1,3 +1,4 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. {{- $kubeTargetVersion := .Capabilities.KubeVersion.GitVersion }} {{ if .Values.ingress.type.kubernetes }} {{- if semverCompare ">=1.19-0" $kubeTargetVersion -}} @@ -146,6 +147,24 @@ spec: serviceName: apim servicePort: {{ printf "%s-broker" .Values.portal.defaultTenantId | quote }} {{- end }} +{{- if .Values.portal.intelligence.enabled }} + - host: {{ include "kafka-proxy-host" . | quote }} + http: + paths: +{{- if semverCompare ">=1.19-0" $kubeTargetVersion }} + - pathType: Prefix + path: "/" + backend: + service: + name: apim + port: + name: kafka-proxy-bootstrap +{{- else }} + - backend: + serviceName: apim + servicePort: kafka-proxy-bootstrap +{{- end }} +{{- end }} {{- range .Values.ingress.customRoutes }} - host: "{{ .subdomain }}.{{ $.Values.portal.domain }}" http: diff --git a/charts/portal/templates/ingress/route.yaml b/charts/portal/templates/ingress/route.yaml index 2c1c7249..9b70433d 100644 --- a/charts/portal/templates/ingress/route.yaml +++ b/charts/portal/templates/ingress/route.yaml @@ -1,3 +1,4 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. {{- if .Values.ingress.type.openshift }} --- apiVersion: route.openshift.io/v1 @@ -125,4 +126,24 @@ spec: name: apim weight: 100 wildcardPolicy: None +{{- if .Values.portal.intelligence.enabled }} +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: apim-route-kafka-proxy + labels: + router: default +spec: + host: {{ include "kafka-proxy-host" . | quote }} + port: + targetPort: kafka-proxy-bootstrap + tls: + termination: passthrough + to: + kind: Service + name: apim + weight: 100 + wildcardPolicy: None +{{- end }} {{- end }} diff --git a/charts/intelligence/templates/server/server-config.yml b/charts/portal/templates/intelligence/intelligence-config.yaml similarity index 67% rename from charts/intelligence/templates/server/server-config.yml rename to charts/portal/templates/intelligence/intelligence-config.yaml index 4ac6649e..29f21a7e 100644 --- a/charts/intelligence/templates/server/server-config.yml +++ b/charts/portal/templates/intelligence/intelligence-config.yaml @@ -1,10 +1,11 @@ +{{- if .Values.portal.intelligence.enabled }} apiVersion: v1 kind: ConfigMap metadata: name: intelligence-server-config labels: app: intelligence-server - chart: {{ template "intelligence.chart" . }} + chart: {{ template "portal.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} data: @@ -17,7 +18,8 @@ data: DATABASE_USE_SSL: {{ .Values.global.databaseUseSSL | quote }} DATABASE_REQUIRE_SSL: {{ .Values.global.databaseRequireSSL | quote }} INTELLIGENCE_DATABASE_NAME: {{ include "intelligence-db-name" . | quote }} - KAFKA_BROKERS: {{ include "kafka-brokers" . | quote}} - PORTAL_DATA_HOST: {{ .Values.intelligenceServer.portalDataHost| default "portal-data:8080" | quote }} + KAFKA_BROKERS: {{ include "intelligence.kafkaBrokers" . | quote }} + PORTAL_DATA_HOST: {{ .Values.intelligence.portalDataHost | default "portal-data:8080" | quote }} RABBITMQ_HOST: {{ .Values.rabbitmq.host | quote }} - RABBITMQ_PORT: {{ .Values.rabbitmq.service.port | quote }} \ No newline at end of file + RABBITMQ_PORT: {{ .Values.rabbitmq.service.port | quote }} +{{- end }} diff --git a/charts/portal/templates/intelligence/intelligence-deployment.yaml b/charts/portal/templates/intelligence/intelligence-deployment.yaml new file mode 100644 index 00000000..e4200d1c --- /dev/null +++ b/charts/portal/templates/intelligence/intelligence-deployment.yaml @@ -0,0 +1,175 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. +{{- if .Values.portal.intelligence.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: intelligence-server + labels: + app: intelligence-server + chart: {{ template "portal.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + {{- range $key, $val := .Values.global.additionalLabels }} + {{ $key }}: "{{ $val }}" + {{- end }} + {{- range $key, $val := .Values.intelligence.additionalLabels }} + {{ $key }}: "{{ $val }}" + {{- end }} +spec: + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + replicas: {{ .Values.intelligence.replicaCount }} + selector: + matchLabels: + app: intelligence-server + template: + metadata: + labels: + app: intelligence-server + release: {{ .Release.Name }} + {{- if .Values.intelligence.podAnnotations }} + annotations: {{- toYaml .Values.intelligence.podAnnotations | nindent 8 }} + {{- if .Values.intelligence.forceRedeploy }} + timestamp: {{ now | quote }} + {{- end }} + {{- end }} + {{- if not .Values.intelligence.podAnnotations }} + {{- if .Values.intelligence.forceRedeploy }} + annotations: + timestamp: {{ now | quote }} + {{- end }} + {{- end }} + spec: + serviceAccountName: {{ include "intelligence.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.intelligence.serviceAccount.automountServiceAccountToken }} + {{- if .Values.intelligence.affinity }} + affinity: {{- toYaml .Values.intelligence.affinity | nindent 12 }} + {{- end }} + {{- if .Values.intelligence.nodeSelector }} + nodeSelector: {{- toYaml .Values.intelligence.nodeSelector | nindent 12 }} + {{- end }} + {{- if .Values.intelligence.tolerations }} + tolerations: {{- toYaml .Values.intelligence.tolerations | nindent 12 }} + {{- end }} + {{- if .Values.intelligence.podSecurityContext }} + securityContext: {{- toYaml .Values.intelligence.podSecurityContext | nindent 12 }} + {{- else if .Values.global.podSecurityContext }} + securityContext: {{- toYaml .Values.global.podSecurityContext | nindent 12 }} + {{- end }} + containers: + - name: intelligence-server + image: "{{ .Values.global.portalRepository }}{{ .Values.image.intelligenceServer }}" + imagePullPolicy: "{{ .Values.intelligence.image.pullPolicy }}" + {{- if .Values.intelligence.containerSecurityContext }} + securityContext: {{- toYaml .Values.intelligence.containerSecurityContext | nindent 12 }} + {{- else if .Values.global.containerSecurityContext }} + securityContext: {{- toYaml .Values.global.containerSecurityContext | nindent 12 }} + {{- end }} + command: ["java"] + args: ["-jar", "/opt/app/intelligence-server.jar"] + env: + - name: RABBITMQ_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: {{ .Values.rabbitmq.auth.secretName | default "rabbitmq" }} + key: rabbitmq-password + optional: false + - name: INTELLIGENCE_DATABASE_NAME + value: {{ template "intelligence-db-name" . }} + - name: DATABASE_USERNAME + value: {{ .Values.global.databaseUsername | quote }} + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.global.databaseSecret }} + {{ if eq .Values.global.databaseType "mysql" }} + key: mysql-password + {{- end }} + - name: KAFKA_BROKERS + value: {{ .Values.intelligence.kafka.brokers | default "kafka:9092" | quote }} + - name: KAFKA_SECURITY_PROTOCOL + value: {{ .Values.intelligence.kafka.securityProtocol | default "PLAINTEXT" | quote }} + {{- if .Values.intelligence.kafka.kafkaCa.caSecretName }} + - name: KAFKA_CA_CERTIFICATE + valueFrom: + secretKeyRef: + name: {{ .Values.intelligence.kafka.kafkaCa.caSecretName }} + key: {{ .Values.intelligence.kafka.kafkaCa.caCertKey | default "apim-ssl.crt" }} + optional: false + - name: KAFKA_CA_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.intelligence.kafka.kafkaCa.caSecretName }} + key: {{ .Values.intelligence.kafka.kafkaCa.caKeyKey | default "apim-ssl.key" }} + optional: false + - name: KAFKA_CA_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.intelligence.kafka.kafkaCa.passwordSecretName }} + key: {{ .Values.intelligence.kafka.kafkaCa.passwordSecretKey | default "keypass.txt" }} + optional: false + - name: TSSG_SSL_KEY_PASS + valueFrom: + secretKeyRef: + name: {{ .Values.intelligence.kafka.kafkaCa.passwordSecretName }} + key: {{ .Values.intelligence.kafka.kafkaCa.passwordSecretKey | default "keypass.txt" }} + optional: false + {{- else }} + - name: KAFKA_CA_CERTIFICATE + value: "" + - name: KAFKA_CA_KEY + value: "" + - name: KAFKA_CA_PASSWORD + value: "" + - name: TSSG_SSL_KEY_PASS + value: "" + {{- end }} + - name: KAFKA_EXTERNAL_ADVERTIZED_BROKERS + value: {{ include "intelligence.kafkaProxyBootstrap" . | quote }} + {{- if .Values.intelligence.env }} + {{- range $key, $value := .Values.intelligence.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- end }} + - name: PAPI_PUBLIC_HOST + value: {{ include "tssg-public-host" . | quote }} + - name: PAPI_PUBLIC_PORT + value: {{ include "tssg-public-port" . | quote }} + envFrom: + - configMapRef: + name: intelligence-server-config + readinessProbe: + httpGet: + path: "/health/readiness" + port: 8282 + initialDelaySeconds: 90 + timeoutSeconds: 1 + periodSeconds: 15 + successThreshold: 1 + livenessProbe: + httpGet: + path: "/health/liveness" + port: 8282 + initialDelaySeconds: 120 + timeoutSeconds: 1 + periodSeconds: 15 + successThreshold: 1 + {{- if .Values.intelligence.resources }} + resources: {{- toYaml .Values.intelligence.resources | nindent 12 }} + {{- end }} + ports: + - containerPort: 8282 + {{- if .Values.global.pullSecret }} + imagePullSecrets: + - name: "{{ .Values.global.pullSecret }}" + {{- end }} + {{- if .Values.global.schedulerName }} + schedulerName: "{{ .Values.global.schedulerName }}" + {{- end }} + restartPolicy: Always + terminationGracePeriodSeconds: 30 +{{- end }} diff --git a/charts/portal/templates/intelligence/intelligence-role.yaml b/charts/portal/templates/intelligence/intelligence-role.yaml new file mode 100644 index 00000000..7774ca03 --- /dev/null +++ b/charts/portal/templates/intelligence/intelligence-role.yaml @@ -0,0 +1,24 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. +{{- if .Values.portal.intelligence.enabled }} +{{- if and .Values.intelligence.rbac.create .Values.intelligence.serviceAccount.create (not .Values.global.serviceAccountName) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: intelligence-server + namespace: {{ .Release.Namespace }} + labels: + app: intelligence-server + chart: {{ template "portal.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +rules: + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +{{- end }} +{{- end }} diff --git a/charts/intelligence/templates/service-accounts/rolebinding.yaml b/charts/portal/templates/intelligence/intelligence-rolebinding.yaml similarity index 51% rename from charts/intelligence/templates/service-accounts/rolebinding.yaml rename to charts/portal/templates/intelligence/intelligence-rolebinding.yaml index 090ac04c..589122ad 100644 --- a/charts/intelligence/templates/service-accounts/rolebinding.yaml +++ b/charts/portal/templates/intelligence/intelligence-rolebinding.yaml @@ -1,19 +1,21 @@ -{{- if and .Values.rbac.create .Values.serviceAccount.create (not .Values.global.serviceAccountName) }} +{{- if .Values.portal.intelligence.enabled }} +{{- if and .Values.intelligence.rbac.create .Values.intelligence.serviceAccount.create (not .Values.global.serviceAccountName) }} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: - name: {{ include "intelligence.fullname" . }} + name: intelligence-server namespace: {{ .Release.Namespace }} labels: - app: {{ template "intelligence.name" . }} - chart: {{ template "intelligence.chart" . }} + app: intelligence-server + chart: {{ template "portal.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role - name: {{ include "intelligence.fullname" . }} + name: intelligence-server subjects: - kind: ServiceAccount name: {{ include "intelligence.serviceAccountName" . }} -{{- end }} \ No newline at end of file +{{- end }} +{{- end }} diff --git a/charts/portal/templates/intelligence/intelligence-service-account.yaml b/charts/portal/templates/intelligence/intelligence-service-account.yaml new file mode 100644 index 00000000..5ee2c6fe --- /dev/null +++ b/charts/portal/templates/intelligence/intelligence-service-account.yaml @@ -0,0 +1,16 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. +{{- if .Values.portal.intelligence.enabled }} +{{- if and .Values.intelligence.serviceAccount.create (not .Values.global.serviceAccountName) }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "intelligence.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + app: intelligence-server + chart: {{ template "portal.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +automountServiceAccountToken: {{ .Values.intelligence.serviceAccount.automountServiceAccountToken }} +{{- end }} +{{- end }} diff --git a/charts/portal/templates/intelligence/intelligence-service.yaml b/charts/portal/templates/intelligence/intelligence-service.yaml new file mode 100644 index 00000000..43bb1cf7 --- /dev/null +++ b/charts/portal/templates/intelligence/intelligence-service.yaml @@ -0,0 +1,33 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. +{{- if .Values.portal.intelligence.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: intelligence-server + labels: + app: intelligence-server + chart: {{ template "portal.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.intelligence.service.type }} + ports: + - name: http + port: {{ .Values.intelligence.service.port }} + targetPort: 8282 + selector: + app: intelligence-server + release: {{ .Release.Name }} + {{- if .Values.intelligence.service.sessionAffinity }} + sessionAffinity: {{ .Values.intelligence.service.sessionAffinity }} + {{- end }} + {{- if .Values.intelligence.service.sessionAffinityConfig }} + sessionAffinityConfig: {{- toYaml .Values.intelligence.service.sessionAffinityConfig | nindent 4 }} + {{- end }} + {{- if .Values.intelligence.service.externalTrafficPolicy }} + externalTrafficPolicy: {{ .Values.intelligence.service.externalTrafficPolicy }} + {{- end }} + {{- if .Values.intelligence.service.internalTrafficPolicy }} + internalTrafficPolicy: {{ .Values.intelligence.service.internalTrafficPolicy }} + {{- end }} +{{- end }} diff --git a/charts/portal/templates/jobs/cert-update-job.yaml b/charts/portal/templates/jobs/cert-update-job.yaml index 0b9cfe21..309b607b 100644 --- a/charts/portal/templates/jobs/cert-update-job.yaml +++ b/charts/portal/templates/jobs/cert-update-job.yaml @@ -1,3 +1,4 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. {{- if .Values.tls.job.enabled }} apiVersion: batch/v1 kind: Job @@ -82,16 +83,6 @@ spec: secretKeyRef: name: {{ template "portal.fullname" . }}-job-secret key: INTERNAL_SECRETNAME - - name: KAFKA_SECRETNAME - valueFrom: - secretKeyRef: - name: {{ template "portal.fullname" . }}-job-secret - key: KAFKA_SECRETNAME - - name: CA_SECRETNAME - valueFrom: - secretKeyRef: - name: {{ template "portal.fullname" . }}-job-secret - key: CA_SECRETNAME - name: EXTERNAL_SECRETNAME valueFrom: secretKeyRef: diff --git a/charts/portal/templates/jobs/job-secret.yaml b/charts/portal/templates/jobs/job-secret.yaml index c24cb38f..f3d69138 100644 --- a/charts/portal/templates/jobs/job-secret.yaml +++ b/charts/portal/templates/jobs/job-secret.yaml @@ -1,3 +1,4 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. apiVersion: v1 kind: Secret metadata: @@ -21,6 +22,4 @@ data: PRIV_KEY_PASS: {{ default "" .Values.tls.keyPass | b64enc | quote }} {{ end }} INTERNAL_SECRETNAME: {{.Values.tls.internalSecretName | b64enc | quote }} - KAFKA_SECRETNAME: {{.Values.tls.kafkaSecretName | b64enc | quote }} - CA_SECRETNAME: {{.Values.tls.kafkaCaSecretName | b64enc | quote }} EXTERNAL_SECRETNAME: {{.Values.tls.externalSecretName | b64enc | quote }} diff --git a/charts/portal/values-production.yaml b/charts/portal/values-production.yaml index 2de69cfa..5edd4ed2 100644 --- a/charts/portal/values-production.yaml +++ b/charts/portal/values-production.yaml @@ -135,10 +135,8 @@ tls: # These secrets are NOT Helm managed, you will need to clean them up manually. # This will force all of the deployments that use the secrets to upgrade # If you don't do this, you will have to perform a manual restart of each affected container - internalSecretName: &internalSecretName portal-internal-secret - externalSecretName: &externalSecretName portal-external-secret - kafkaCaSecretName: &kafkaCaSecretName portal-kafka-ca-secret - kafkaSecretName: portal-kafka-tls-secret + internalSecretName: portal-internal-secret + externalSecretName: portal-external-secret useSignedCertificates: false # crt, crtChain and key expect files, use --set-file or add them here @@ -600,7 +598,8 @@ image: data: portal-data:5.4.1 tps: tenant-provisioning-service:5.4.1 analytics: analytics-server:5.4.1 - authenticator: authenticator:5.4.1.2 + authenticator: authenticator:5.4.1 + intelligenceServer: intelligence-server:5.4.1 dbUpgrade: db-upgrade-portal:5.4.1 rbacUpgrade: db-upgrade-rbac:5.4.1 upgradeVerify: upgrade-verify:5.4.1 @@ -1141,7 +1140,7 @@ druid: # Configuration for the custom Kafka subchart -kafka: &kafka_config +kafka: # Enable/disable Kafka deployment # Automatically enabled when analytics or intelligence is enabled # Set to false to explicitly disable Kafka even when analytics/intelligence are enabled @@ -1215,12 +1214,6 @@ kafka: &kafka_config enabled: true port: 9092 protocol: PLAINTEXT - # External listener for clients outside the cluster (third-party agents) - # NOTE: Automatically controlled by kafka.externalAccess.enabled - # External listener is created only when intelligence is enabled - external: - port: 9094 - protocol: SSL # Controller listener for KRaft mode controller: enabled: true @@ -1232,24 +1225,6 @@ kafka: &kafka_config # Log retention configuration logRetentionHours: 6 - # TLS/SSL Configuration - tls: - # Enable TLS/SSL - enabled by default for external SSL support - enabled: true - # TLS type (PEM or JKS) - type: PEM - # Client authentication (none, requested, required) - clientAuth: required - # Secret containing TLS certificates (via environment variables) - # Leave empty to use volume-mounted certificates from internalSecretName instead - secretName: "" - # Keys in the secret - keystoreKeyKey: "keystore.key" - truststoreCertKey: "truststore.pem" - # Password secret for encrypted keys - passwordSecretName: "" - passwordSecretKey: "keypass.txt" - # SASL Authentication Configuration sasl: # Enable SASL authentication @@ -1278,44 +1253,8 @@ kafka: &kafka_config name: "" automountServiceAccountToken: true - # External access configuration for autodiscovery - # NOTE: External access is automatically enabled when portal.intelligence.enabled is true - # Intelligence requires external Kafka access for third-party agents - # Default is false. Set to true manually or enable intelligence feature. - externalAccess: - enabled: false - # Service type for external access (LoadBalancer or NodePort) - serviceType: LoadBalancer - # Port for external access - port: 9094 - # Hostname/domain for external access (used in advertised listeners) - hostname: "" - # Annotations for external services - annotations: - cloud.google.com/load-balancer-type: Internal - # LoadBalancer IPs (optional, for static IPs) - loadBalancerIPs: [] - # NodePort values (optional, for NodePort service type) - nodePorts: [] - # Auto-generate advertised listeners based on service IPs - autoAdvertisedListeners: true - # Discovery configuration for LoadBalancer/NodePort IP detection - # NOTE: This only applies when externalAccess.enabled is true (i.e., when intelligence is enabled) - # When intelligence is disabled, Kafka uses internal-only listeners and will start normally - discovery: - # Maximum time to wait for LoadBalancer IP (in seconds) - # Increase this if your cloud provider takes longer to provision LoadBalancers - timeoutSeconds: 120 - # Retry interval (in seconds) - retryIntervalSeconds: 5 - # Fail fast: If true, pod will fail when LoadBalancer IP cannot be discovered - # This prevents Kafka from starting with incorrect advertised listeners - # IMPORTANT: Only affects behavior when intelligence is enabled - # Recommended: true (prevents connection issues for external clients) - failOnDiscoveryFailure: true - # Settings for RabbitMQ - https://github.com/bitnami/charts/tree/master/bitnami/rabbitmq -rabbitmq: &rabbitmq_config +rabbitmq: enabled: true host: rabbitmq fullnameOverride: rabbitmq @@ -1408,22 +1347,11 @@ rabbitmq: &rabbitmq_config - -ec - curl -f --user {{ .Values.auth.username }}:$RABBITMQ_PASSWORD 127.0.0.1:{{ .Values.containerPorts.manager }}/api/health/checks/local-alarms -# Settings for the intelligence subchart -apim-intelligence: - # Use a YAML alias (*) to reference the Kafka configuration from above. - # This avoids duplicating the configuration and ensures the intelligence - # subchart has the correct values for broker discovery. - kafka: *kafka_config - rabbitmq: *rabbitmq_config - - # NOTE: Do NOT set portal.domain here - it will override user values - # Intelligence will automatically use portal.domain from parent chart if available - # Jenkins must set: portal.domain AND apim-intelligence.portal.domain to the same value - - # Image configuration +# Intelligence Server configuration +intelligence: + forceRedeploy: false + replicaCount: 1 image: - autoDiscovery: tls-automator:5.4.1 # Used for Kafka broker discovery init container (Overridden by Jenkins with TAG_TLS_MANAGER) - intelligenceServer: intelligence-server:5.4.1.2 pullPolicy: IfNotPresent # RBAC configuration @@ -1436,54 +1364,43 @@ apim-intelligence: automountServiceAccountToken: true name: "" - intelligenceServer: - # Configuration for the Intelligence Server. - kafka: - # A list of external advertised Kafka brokers. - # externalAdvertisedBrokers: - # Configuration for Kafka broker auto-discovery. - autoDiscovery: - # Enables or disables Kafka broker auto-discovery. - enabled: true - image: - # The repository of the auto-discovery image. - repository: "docker.io/caapim/kubectl" - # The tag of the auto-discovery image. - tag: "1.33.3-debian-12-r0" - # The pull policy for the auto-discovery image. - pullPolicy: IfNotPresent - # Resource requests and limits for the auto-discovery init container - # resources: {} - # Configuration for the Kafka CA certificate. - kafkaCa: - # The name of the secret containing the Kafka CA certificate. - caSecretName: *externalSecretName - # The name of the secret containing the password for the Kafka CA certificate. - passwordSecretName: *externalSecretName - # The key in the password secret that contains the password. - passwordSecretKey: "keypass.txt" - forceRedeploy: false - pdb: - create: false - maxUnavailable: "" - minAvailable: "" - replicaCount: 1 - image: - pullPolicy: IfNotPresent - # ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ - podAnnotations: {} - # ref:https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod - podSecurityContext: {} - # ref:https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container - containerSecurityContext: {} - resources: - requests: {} - # memory: 4Gi - limits: {} - # memory: 4Gi - # nodeSelector: {} - # tolerations: [] - # affinity: {} + # Kafka configuration for the intelligence server + kafka: + brokers: "kafka:9092" + securityProtocol: PLAINTEXT + kafkaCa: + caSecretName: portal-external-secret + passwordSecretName: portal-external-secret + passwordSecretKey: "keypass.txt" + caCertKey: "apim-ssl.crt" + caKeyKey: "apim-ssl.key" + + # Service configuration + service: + type: ClusterIP + port: 8282 + sessionAffinity: None + externalTrafficPolicy: "" + internalTrafficPolicy: "" + + portalDataHost: "portal-data:8080" + + pdb: + create: false + maxUnavailable: "" + minAvailable: "" + podAnnotations: {} + podSecurityContext: {} + containerSecurityContext: {} + resources: + requests: {} + # memory: 4Gi + limits: {} + # memory: 4Gi + additionalLabels: {} + affinity: {} + nodeSelector: {} + tolerations: [] # Settings for Nginx-Ingress - https://github.com/kubernetes/ingress-nginx/tree/master/charts/ingress-nginx ingress-nginx: diff --git a/charts/portal/values.yaml b/charts/portal/values.yaml index 0c2e1993..01d83206 100644 --- a/charts/portal/values.yaml +++ b/charts/portal/values.yaml @@ -75,6 +75,18 @@ portal: # Intelligence is disabled by default. Enable it for third-party agent integration. intelligence: enabled: false + # KafkaTcpProxy configuration on APIM Gateway + kafkaProxy: + # Internal Kafka bootstrap servers that the proxy forwards traffic to + targetBootstrapServers: "kafka:9092" + # Bootstrap listen port on the APIM Gateway (KafkaTcpProxy assertion) + listenPort: 9192 + # Per-broker listen ports start from this value (9193, 9194, 9195, ...) + brokerStartPort: 9193 + # Hostname advertised to Kafka clients in metadata responses. + # Defaults to the APIM Gateway's public hostname (tssg-public-host). + # Must be externally reachable by TSSGs connecting through the proxy. + advertisedHost: "" # Please set analytics.replicaCount to a minimum of 2 aggregation: false # Specify a Gateway v9.x license file via set portal.license.value @@ -138,10 +150,8 @@ tls: # These secrets are NOT Helm managed, you will need to clean them up manually. # This will force all of the deployments that use the secrets to upgrade # If you don't do this, you will have to perform a manual restart of each affected container - internalSecretName: &internalSecretName portal-internal-secret - externalSecretName: &externalSecretName portal-external-secret - kafkaCaSecretName: &kafkaCaSecretName portal-kafka-ca-secret - kafkaSecretName: portal-kafka-tls-secret + internalSecretName: portal-internal-secret + externalSecretName: portal-external-secret useSignedCertificates: false # crt, crtChain and key expect files, use --set-file or add them here @@ -533,7 +543,8 @@ image: data: portal-data:5.4.1 tps: tenant-provisioning-service:5.4.1 analytics: analytics-server:5.4.1 - authenticator: authenticator:5.4.1.2 + authenticator: authenticator:5.4.1 + intelligenceServer: intelligence-server:5.4.1 dbUpgrade: db-upgrade-portal:5.4.1 rbacUpgrade: db-upgrade-rbac:5.4.1 upgradeVerify: upgrade-verify:5.4.1 @@ -987,7 +998,7 @@ druid: # Configuration for the custom Kafka subchart -kafka: &kafka_config +kafka: # Enable/disable Kafka deployment # Automatically enabled when analytics or intelligence is enabled # Set to false to explicitly disable Kafka even when analytics/intelligence are enabled @@ -1061,12 +1072,6 @@ kafka: &kafka_config enabled: true port: 9092 protocol: PLAINTEXT - # External listener for clients outside the cluster (third-party agents) - # NOTE: Automatically controlled by kafka.externalAccess.enabled - # External listener is created only when intelligence is enabled - external: - port: 9094 - protocol: SSL # Controller listener for KRaft mode controller: enabled: true @@ -1078,24 +1083,6 @@ kafka: &kafka_config # Log retention configuration logRetentionHours: 6 - # TLS/SSL Configuration - tls: - # Enable TLS/SSL - enabled by default for external SSL support - enabled: true - # TLS type (PEM or JKS) - type: PEM - # Client authentication (none, requested, required) - clientAuth: required - # Secret containing TLS certificates (via environment variables) - # Leave empty to use volume-mounted certificates from internalSecretName instead - secretName: "" - # Keys in the secret - keystoreKeyKey: "keystore.key" - truststoreCertKey: "truststore.pem" - # Password secret for encrypted keys - passwordSecretName: "" - passwordSecretKey: "keypass.txt" - # SASL Authentication Configuration sasl: # Enable SASL authentication @@ -1119,49 +1106,13 @@ kafka: &kafka_config # Service Account serviceAccount: - create: true + create: false annotations: {} name: "" - automountServiceAccountToken: true - - # External access configuration for autodiscovery - # NOTE: External access is automatically enabled when portal.intelligence.enabled is true - # Intelligence requires external Kafka access for third-party agents - # Default is false. Set to true manually or enable intelligence feature. - externalAccess: - enabled: false - # Service type for external access (LoadBalancer or NodePort) - serviceType: LoadBalancer - # Port for external access - port: 9094 - # Hostname/domain for external access (used in advertised listeners) - hostname: "" - # Annotations for external services - annotations: - cloud.google.com/load-balancer-type: Internal - # LoadBalancer IPs (optional, for static IPs) - loadBalancerIPs: [] - # NodePort values (optional, for NodePort service type) - nodePorts: [] - # Auto-generate advertised listeners based on service IPs - autoAdvertisedListeners: true - # Discovery configuration for LoadBalancer/NodePort IP detection - # NOTE: This only applies when externalAccess.enabled is true (i.e., when intelligence is enabled) - # When intelligence is disabled, Kafka uses internal-only listeners and will start normally - discovery: - # Maximum time to wait for LoadBalancer IP (in seconds) - # Increase this if your cloud provider takes longer to provision LoadBalancers - timeoutSeconds: 120 - # Retry interval (in seconds) - retryIntervalSeconds: 5 - # Fail fast: If true, pod will fail when LoadBalancer IP cannot be discovered - # This prevents Kafka from starting with incorrect advertised listeners - # IMPORTANT: Only affects behavior when intelligence is enabled - # Recommended: true (prevents connection issues for external clients) - failOnDiscoveryFailure: true + automountServiceAccountToken: false # Settings for RabbitMQ - https://github.com/bitnami/charts/tree/master/bitnami/rabbitmq -rabbitmq: &rabbitmq_config +rabbitmq: enabled: true host: rabbitmq fullnameOverride: rabbitmq @@ -1244,82 +1195,63 @@ rabbitmq: &rabbitmq_config - -ec - curl -f --user {{ .Values.auth.username }}:$RABBITMQ_PASSWORD 127.0.0.1:{{ .Values.containerPorts.manager }}/api/health/checks/local-alarms -# Settings for the intelligence subchart -apim-intelligence: - # Use a YAML alias (*) to reference the Kafka configuration from above. - # This avoids duplicating the configuration and ensures the intelligence - # subchart has the correct values for broker discovery. - kafka: *kafka_config - rabbitmq: *rabbitmq_config - - # NOTE: Do NOT set portal.domain here - it will override user values - # Intelligence will automatically use portal.domain from parent chart if available - # Jenkins must set: portal.domain AND apim-intelligence.portal.domain to the same value - - # Image configuration +# Intelligence Server configuration +intelligence: + forceRedeploy: false + replicaCount: 1 image: - autoDiscovery: tls-automator:5.4.1 # Used for Kafka broker discovery init container (Overridden by Jenkins with TAG_TLS_MANAGER) - intelligenceServer: intelligence-server:5.4.1.2 pullPolicy: IfNotPresent # RBAC configuration rbac: - create: true + create: false # Service Account configuration serviceAccount: - create: true - automountServiceAccountToken: true + create: false + automountServiceAccountToken: false name: "" - intelligenceServer: - # Configuration for the Intelligence Server. - kafka: - # A list of external advertised Kafka brokers. - # externalAdvertisedBrokers: - # Configuration for Kafka broker auto-discovery. - autoDiscovery: - # Enables or disables Kafka broker auto-discovery. - enabled: true - image: - # The repository of the auto-discovery image. - repository: "docker.io/caapim/kubectl" - # The tag of the auto-discovery image. - tag: "1.33.3-debian-12-r0" - # The pull policy for the auto-discovery image. - pullPolicy: IfNotPresent - # Resource requests and limits for the auto-discovery init container - # resources: {} - # Configuration for the Kafka CA certificate. - kafkaCa: - # The name of the secret containing the Kafka CA certificate. - caSecretName: *externalSecretName - # The name of the secret containing the password for the Kafka CA certificate. - passwordSecretName: *externalSecretName - # The key in the password secret that contains the password. - passwordSecretKey: "keypass.txt" - forceRedeploy: false - pdb: - create: false - maxUnavailable: "" - minAvailable: "" - replicaCount: 1 - image: - pullPolicy: IfNotPresent - # ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ - podAnnotations: {} - # ref:https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod - podSecurityContext: {} - # ref:https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container - containerSecurityContext: {} - resources: - requests: {} - # memory: 4Gi - limits: {} - # memory: 4Gi - # nodeSelector: {} - # tolerations: [] - # affinity: {} + # Kafka configuration for the intelligence server + kafka: + brokers: "kafka:9092" + securityProtocol: PLAINTEXT + kafkaCa: + caSecretName: portal-external-secret + passwordSecretName: portal-external-secret + passwordSecretKey: "keypass.txt" + caCertKey: "apim-ssl.crt" + caKeyKey: "apim-ssl.key" + + # Service configuration + service: + type: ClusterIP + port: 8282 + sessionAffinity: None + externalTrafficPolicy: "" + internalTrafficPolicy: "" + + portalDataHost: "portal-data:8080" + + pdb: + create: false + maxUnavailable: "" + minAvailable: "" + # ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + podAnnotations: {} + # ref:https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + podSecurityContext: {} + # ref:https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container + containerSecurityContext: {} + resources: + requests: {} + # memory: 4Gi + limits: {} + # memory: 4Gi + additionalLabels: {} + affinity: {} + nodeSelector: {} + tolerations: [] # Settings for Nginx-Ingress - https://github.com/kubernetes/ingress-nginx/tree/master/charts/ingress-nginx ingress-nginx: From 1f4abb574d395ccb91efa85a355a0c29e1c68615 Mon Sep 17 00:00:00 2001 From: Kiran Vaddadi Date: Fri, 13 Mar 2026 22:04:42 +0530 Subject: [PATCH 2/6] SNI-based per-broker routing for Kafka TCP Proxy across all ingress types Add per-broker SNI hostname generation and dynamic ingress/route/tlsroute rules for Kafka TCP Proxy. Each Kafka broker gets a unique hostname (e.g., kafka-proxy-1.domain) routed via TLS passthrough on port 443, eliminating the need for multiple external ports. Shorten K8s service port names to fit 15-char limit (kfk-proxy-boot, kfk-proxy-brk-N). Update brokerStartPort default to 9194 to avoid port collision with listenPort after init-tssh.sh -1 adjustment. Add advertisedPort and brokerAddressPattern config values. Remove intelligence-specific RBAC in favor of shared service account. Made-with: Cursor --- charts/portal/templates/_helpers.tpl | 41 ++++++++++-------- charts/portal/templates/apim/apim-config.yaml | 6 ++- .../portal/templates/apim/apim-service.yaml | 9 ++-- .../portal/templates/gateway-api/gateway.yaml | 11 ++++- .../templates/gateway-api/tlsroute.yaml | 42 +++++++++++++++++++ .../templates/ingress/contour-httpproxy.yaml | 35 ++++++++++++++++ charts/portal/templates/ingress/ingress.yaml | 28 ++++++++++++- charts/portal/templates/ingress/route.yaml | 28 ++++++++++++- .../intelligence/intelligence-deployment.yaml | 3 +- .../intelligence/intelligence-role.yaml | 24 ----------- .../intelligence-rolebinding.yaml | 21 ---------- .../intelligence-service-account.yaml | 16 ------- charts/portal/values-production.yaml | 10 ----- charts/portal/values.yaml | 28 ++++++------- 14 files changed, 185 insertions(+), 117 deletions(-) delete mode 100644 charts/portal/templates/intelligence/intelligence-role.yaml delete mode 100644 charts/portal/templates/intelligence/intelligence-rolebinding.yaml delete mode 100644 charts/portal/templates/intelligence/intelligence-service-account.yaml diff --git a/charts/portal/templates/_helpers.tpl b/charts/portal/templates/_helpers.tpl index 1b13d4cf..a6048e38 100644 --- a/charts/portal/templates/_helpers.tpl +++ b/charts/portal/templates/_helpers.tpl @@ -433,6 +433,27 @@ Used when intelligence is enabled. {{- end }} {{- end -}} +{{/* +Generate per-broker hostname for a given nodeId using the kafka-proxy-host base. +Produces pattern like: dev-portal-kafka-proxy-$(nodeId).example.com +Used as the default for KAFKA_PROXY_BROKER_ADDRESS_PATTERN. +*/}} +{{- define "kafka-proxy-broker-pattern" -}} + {{- $base := include "kafka-proxy-host" . -}} + {{- $parts := splitn "." 2 $base -}} + {{- printf "%s-$(nodeId).%s" (index $parts "_0") (index $parts "_1") -}} +{{- end -}} + +{{/* +Generate per-broker hostname for a specific nodeId (for ingress routes). +Takes a dict with "root" (context) and "nodeId" (int). +*/}} +{{- define "kafka-proxy-broker-host" -}} + {{- $base := include "kafka-proxy-host" .root -}} + {{- $parts := splitn "." 2 $base -}} + {{- printf "%s-%s.%s" (index $parts "_0") (toString .nodeId) (index $parts "_1") -}} +{{- end -}} + {{/* Generate default parentRefs for routes when none are provided. Produces a list with a single parentRef pointing to the chart's Gateway. @@ -464,21 +485,6 @@ Get "intelligence" database name {{- end }} {{- end -}} -{{/* -Set the service account name for the Intelligence Server -*/}} -{{- define "intelligence.serviceAccountName" -}} -{{- if .Values.global.serviceAccountName }} - {{ default "default" .Values.global.serviceAccountName }} -{{- else }} -{{- if .Values.intelligence.serviceAccount.create -}} - {{ default "intelligence-server" .Values.intelligence.serviceAccount.name }} -{{- else -}} - {{ default "default" .Values.intelligence.serviceAccount.name }} -{{- end -}} -{{- end -}} -{{- end -}} - {{/* Get Kafka broker address for intelligence server. Uses the kafka subchart's fullnameOverride and internal listener port. @@ -494,11 +500,10 @@ Uses the kafka subchart's fullnameOverride and internal listener port. {{/* Generate Kafka proxy bootstrap address for intelligence server. -Format: {kafka-proxy-host}:{listenPort} -Reuses the kafka-proxy-host helper and the kafkaProxy.listenPort value. +Uses the advertised port (default 443) for SNI-based routing. */}} {{- define "intelligence.kafkaProxyBootstrap" -}} {{- $host := include "kafka-proxy-host" . -}} - {{- $port := int (.Values.portal.intelligence.kafkaProxy.listenPort | default 9192) -}} + {{- $port := int (.Values.portal.intelligence.kafkaProxy.advertisedPort | default 443) -}} {{- printf "%s:%d" $host $port -}} {{- end -}} diff --git a/charts/portal/templates/apim/apim-config.yaml b/charts/portal/templates/apim/apim-config.yaml index 6afebc73..454f945a 100644 --- a/charts/portal/templates/apim/apim-config.yaml +++ b/charts/portal/templates/apim/apim-config.yaml @@ -34,8 +34,10 @@ data: INTELLIGENCE_ENABLED: "true" KAFKA_PROXY_TARGET_SERVERS: {{ default "kafka:9092" .Values.portal.intelligence.kafkaProxy.targetBootstrapServers | quote }} KAFKA_PROXY_LISTEN_PORT: {{ default 9192 .Values.portal.intelligence.kafkaProxy.listenPort | quote }} - KAFKA_PROXY_BROKER_START_PORT: {{ default 9193 .Values.portal.intelligence.kafkaProxy.brokerStartPort | quote }} - KAFKA_PROXY_ADVERTISED_HOST: {{ default (include "tssg-public-host" .) .Values.portal.intelligence.kafkaProxy.advertisedHost | quote }} + KAFKA_PROXY_BROKER_START_PORT: {{ default 9194 .Values.portal.intelligence.kafkaProxy.brokerStartPort | quote }} + KAFKA_PROXY_ADVERTISED_HOST: {{ default (include "kafka-proxy-host" .) .Values.portal.intelligence.kafkaProxy.advertisedHost | quote }} + KAFKA_PROXY_ADVERTISED_PORT: {{ default 443 .Values.portal.intelligence.kafkaProxy.advertisedPort | quote }} + KAFKA_PROXY_BROKER_ADDRESS_PATTERN: {{ default (include "kafka-proxy-broker-pattern" .) .Values.portal.intelligence.kafkaProxy.brokerAddressPattern | quote }} {{- else }} INTELLIGENCE_ENABLED: "false" {{- end }} diff --git a/charts/portal/templates/apim/apim-service.yaml b/charts/portal/templates/apim/apim-service.yaml index 74c3e5df..5912120c 100644 --- a/charts/portal/templates/apim/apim-service.yaml +++ b/charts/portal/templates/apim/apim-service.yaml @@ -46,19 +46,20 @@ spec: - port: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} targetPort: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} protocol: TCP - name: kafka-proxy-bootstrap -{{- $brokerStartPort := int (.Values.portal.intelligence.kafkaProxy.brokerStartPort | default 9193) }} + name: kfk-proxy-boot +{{- $brokerStartPort := int (.Values.portal.intelligence.kafkaProxy.brokerStartPort | default 9194) }} {{- $replicaCount := int (.Values.kafka.kafka.replicaCount | default 1) }} {{- $ordinalStart := 0 }} {{- if and .Values.kafka.kafka.ordinals (hasKey .Values.kafka.kafka.ordinals "start") }} {{- $ordinalStart = int .Values.kafka.kafka.ordinals.start }} {{- end }} {{- range $i := until $replicaCount }} -{{- $brokerPort := add $brokerStartPort (add $ordinalStart $i) }} +{{- $nodeId := add $ordinalStart $i }} +{{- $brokerPort := add (sub $brokerStartPort 1) $nodeId }} - port: {{ $brokerPort }} targetPort: {{ $brokerPort }} protocol: TCP - name: kafka-proxy-broker-{{ add $ordinalStart $i }} + name: kfk-proxy-brk-{{ $nodeId }} {{- end }} {{- end }} selector: diff --git a/charts/portal/templates/gateway-api/gateway.yaml b/charts/portal/templates/gateway-api/gateway.yaml index 4e7f42f6..4ec521a8 100644 --- a/charts/portal/templates/gateway-api/gateway.yaml +++ b/charts/portal/templates/gateway-api/gateway.yaml @@ -41,9 +41,18 @@ spec: {{- $hostnames = append $hostnames (include "pssg-sync-host" .) }} {{- $hostnames = append $hostnames (include "pssg-sso-host" .) }} {{- $hostnames = append $hostnames (include "broker-host" .) }} - {{/* Kafka proxy route */}} + {{/* Kafka proxy routes: bootstrap + per-broker SNI hostnames */}} {{- if .Values.portal.intelligence.enabled }} {{- $hostnames = append $hostnames (include "kafka-proxy-host" .) }} + {{- $replicaCount := int (.Values.kafka.kafka.replicaCount | default 1) }} + {{- $ordinalStart := 0 }} + {{- if and .Values.kafka.kafka.ordinals (hasKey .Values.kafka.kafka.ordinals "start") }} + {{- $ordinalStart = int .Values.kafka.kafka.ordinals.start }} + {{- end }} + {{- range $i := until $replicaCount }} + {{- $nodeId := add $ordinalStart $i }} + {{- $hostnames = append $hostnames (include "kafka-proxy-broker-host" (dict "root" $ "nodeId" $nodeId)) }} + {{- end }} {{- end }} {{/* Dynamic tenant routes */}} {{- range .Values.ingress.tenantIds }} diff --git a/charts/portal/templates/gateway-api/tlsroute.yaml b/charts/portal/templates/gateway-api/tlsroute.yaml index eaebdf53..3fbb9e20 100644 --- a/charts/portal/templates/gateway-api/tlsroute.yaml +++ b/charts/portal/templates/gateway-api/tlsroute.yaml @@ -265,6 +265,48 @@ spec: - backendRefs: - name: apim port: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} +{{- $brokerStartPort := int (.Values.portal.intelligence.kafkaProxy.brokerStartPort | default 9193) }} +{{- $replicaCount := int (.Values.kafka.kafka.replicaCount | default 1) }} +{{- $ordinalStart := 0 }} +{{- if and .Values.kafka.kafka.ordinals (hasKey .Values.kafka.kafka.ordinals "start") }} +{{- $ordinalStart = int .Values.kafka.kafka.ordinals.start }} +{{- end }} +{{- range $i := until $replicaCount }} +{{- $nodeId := add $ordinalStart $i }} +{{- $brokerPort := add $brokerStartPort $nodeId }} +--- +apiVersion: {{ include "portal.gatewayAPI.tlsRouteApiVersion" $root }} +kind: TLSRoute +metadata: + name: portal-tlsroute-kafka-broker-{{ $nodeId }} + namespace: {{ $.Release.Namespace }} + labels: + app: {{ template "portal.name" $root }} + chart: {{ template "portal.chart" $root }} + release: {{ $root.Release.Name }} + heritage: {{ $root.Release.Service }} + {{- range $key, $val := $root.Values.global.additionalLabels }} + {{ $key }}: "{{ $val }}" + {{- end }} + {{- range $key, $val := $root.Values.ingress.gatewayAPI.labels }} + {{ $key }}: "{{ $val }}" + {{- end }} + {{- if $annotations }} + annotations: + {{- range $key, $val := $annotations }} + {{ $key }}: "{{ $val }}" + {{- end }} + {{- end }} +spec: + parentRefs: + {{- include "portal.gatewayAPI.defaultParentRefs" $root | nindent 4 }} + hostnames: + - {{ include "kafka-proxy-broker-host" (dict "root" $root "nodeId" $nodeId) | quote }} + rules: + - backendRefs: + - name: apim + port: {{ $brokerPort }} +{{- end }} {{- end }} {{/* ============================================================ diff --git a/charts/portal/templates/ingress/contour-httpproxy.yaml b/charts/portal/templates/ingress/contour-httpproxy.yaml index c0cd20bd..9d2bab6a 100644 --- a/charts/portal/templates/ingress/contour-httpproxy.yaml +++ b/charts/portal/templates/ingress/contour-httpproxy.yaml @@ -202,6 +202,41 @@ spec: services: - name: apim port: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} +{{- $brokerStartPort := int (.Values.portal.intelligence.kafkaProxy.brokerStartPort | default 9193) }} +{{- $replicaCount := int (.Values.kafka.kafka.replicaCount | default 1) }} +{{- $ordinalStart := 0 }} +{{- if and .Values.kafka.kafka.ordinals (hasKey .Values.kafka.kafka.ordinals "start") }} +{{- $ordinalStart = int .Values.kafka.kafka.ordinals.start }} +{{- end }} +{{- range $i := until $replicaCount }} +{{- $nodeId := add $ordinalStart $i }} +{{- $brokerPort := add $brokerStartPort $nodeId }} +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: portal-kafka-broker-{{ $nodeId }}-httpproxy + labels: + app: {{ template "portal.name" $ }} + chart: {{ template "portal.chart" $ }} + release: {{ $.Release.Name }} + heritage: {{ $.Release.Service }} + {{- range $key, $val := $.Values.global.additionalLabels }} + {{ $key }}: "{{ $val }}" + {{- end }} + {{- range $key, $val := $.Values.ingress.additionalLabels }} + {{ $key }}: "{{ $val }}" + {{- end }} +spec: + virtualhost: + fqdn: {{ include "kafka-proxy-broker-host" (dict "root" $ "nodeId" $nodeId) | quote }} + tls: + passthrough: true + tcpproxy: + services: + - name: apim + port: {{ $brokerPort }} +{{- end }} {{- end }} {{- range $i,$tenantId := $.Values.ingress.tenantIds }} diff --git a/charts/portal/templates/ingress/ingress.yaml b/charts/portal/templates/ingress/ingress.yaml index f34b74ff..e471a6e5 100644 --- a/charts/portal/templates/ingress/ingress.yaml +++ b/charts/portal/templates/ingress/ingress.yaml @@ -158,11 +158,35 @@ spec: service: name: apim port: - name: kafka-proxy-bootstrap + name: kfk-proxy-boot {{- else }} - backend: serviceName: apim - servicePort: kafka-proxy-bootstrap + servicePort: kfk-proxy-boot +{{- end }} +{{- $replicaCount := int (.Values.kafka.kafka.replicaCount | default 1) }} +{{- $ordinalStart := 0 }} +{{- if and .Values.kafka.kafka.ordinals (hasKey .Values.kafka.kafka.ordinals "start") }} +{{- $ordinalStart = int .Values.kafka.kafka.ordinals.start }} +{{- end }} +{{- range $i := until $replicaCount }} +{{- $nodeId := add $ordinalStart $i }} + - host: {{ include "kafka-proxy-broker-host" (dict "root" $ "nodeId" $nodeId) | quote }} + http: + paths: +{{- if semverCompare ">=1.19-0" $kubeTargetVersion }} + - pathType: Prefix + path: "/" + backend: + service: + name: apim + port: + name: kfk-proxy-brk-{{ $nodeId }} +{{- else }} + - backend: + serviceName: apim + servicePort: kfk-proxy-brk-{{ $nodeId }} +{{- end }} {{- end }} {{- end }} {{- range .Values.ingress.customRoutes }} diff --git a/charts/portal/templates/ingress/route.yaml b/charts/portal/templates/ingress/route.yaml index 9b70433d..08c00e43 100644 --- a/charts/portal/templates/ingress/route.yaml +++ b/charts/portal/templates/ingress/route.yaml @@ -137,7 +137,7 @@ metadata: spec: host: {{ include "kafka-proxy-host" . | quote }} port: - targetPort: kafka-proxy-bootstrap + targetPort: kfk-proxy-boot tls: termination: passthrough to: @@ -145,5 +145,31 @@ spec: name: apim weight: 100 wildcardPolicy: None +{{- $replicaCount := int (.Values.kafka.kafka.replicaCount | default 1) }} +{{- $ordinalStart := 0 }} +{{- if and .Values.kafka.kafka.ordinals (hasKey .Values.kafka.kafka.ordinals "start") }} +{{- $ordinalStart = int .Values.kafka.kafka.ordinals.start }} +{{- end }} +{{- range $i := until $replicaCount }} +{{- $nodeId := add $ordinalStart $i }} +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: apim-route-kafka-broker-{{ $nodeId }} + labels: + router: default +spec: + host: {{ include "kafka-proxy-broker-host" (dict "root" $ "nodeId" $nodeId) | quote }} + port: + targetPort: kfk-proxy-brk-{{ $nodeId }} + tls: + termination: passthrough + to: + kind: Service + name: apim + weight: 100 + wildcardPolicy: None +{{- end }} {{- end }} {{- end }} diff --git a/charts/portal/templates/intelligence/intelligence-deployment.yaml b/charts/portal/templates/intelligence/intelligence-deployment.yaml index e4200d1c..b3688baa 100644 --- a/charts/portal/templates/intelligence/intelligence-deployment.yaml +++ b/charts/portal/templates/intelligence/intelligence-deployment.yaml @@ -43,8 +43,7 @@ spec: {{- end }} {{- end }} spec: - serviceAccountName: {{ include "intelligence.serviceAccountName" . }} - automountServiceAccountToken: {{ .Values.intelligence.serviceAccount.automountServiceAccountToken }} + serviceAccountName: {{ include "portal.serviceAccountName" . }} {{- if .Values.intelligence.affinity }} affinity: {{- toYaml .Values.intelligence.affinity | nindent 12 }} {{- end }} diff --git a/charts/portal/templates/intelligence/intelligence-role.yaml b/charts/portal/templates/intelligence/intelligence-role.yaml deleted file mode 100644 index 7774ca03..00000000 --- a/charts/portal/templates/intelligence/intelligence-role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. -{{- if .Values.portal.intelligence.enabled }} -{{- if and .Values.intelligence.rbac.create .Values.intelligence.serviceAccount.create (not .Values.global.serviceAccountName) }} -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: intelligence-server - namespace: {{ .Release.Namespace }} - labels: - app: intelligence-server - chart: {{ template "portal.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -rules: - - apiGroups: - - "" - resources: - - services - verbs: - - get - - list - - watch -{{- end }} -{{- end }} diff --git a/charts/portal/templates/intelligence/intelligence-rolebinding.yaml b/charts/portal/templates/intelligence/intelligence-rolebinding.yaml deleted file mode 100644 index 589122ad..00000000 --- a/charts/portal/templates/intelligence/intelligence-rolebinding.yaml +++ /dev/null @@ -1,21 +0,0 @@ -{{- if .Values.portal.intelligence.enabled }} -{{- if and .Values.intelligence.rbac.create .Values.intelligence.serviceAccount.create (not .Values.global.serviceAccountName) }} -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: intelligence-server - namespace: {{ .Release.Namespace }} - labels: - app: intelligence-server - chart: {{ template "portal.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: intelligence-server -subjects: - - kind: ServiceAccount - name: {{ include "intelligence.serviceAccountName" . }} -{{- end }} -{{- end }} diff --git a/charts/portal/templates/intelligence/intelligence-service-account.yaml b/charts/portal/templates/intelligence/intelligence-service-account.yaml deleted file mode 100644 index 5ee2c6fe..00000000 --- a/charts/portal/templates/intelligence/intelligence-service-account.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. -{{- if .Values.portal.intelligence.enabled }} -{{- if and .Values.intelligence.serviceAccount.create (not .Values.global.serviceAccountName) }} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "intelligence.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} - labels: - app: intelligence-server - chart: {{ template "portal.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -automountServiceAccountToken: {{ .Values.intelligence.serviceAccount.automountServiceAccountToken }} -{{- end }} -{{- end }} diff --git a/charts/portal/values-production.yaml b/charts/portal/values-production.yaml index 5edd4ed2..dea6d359 100644 --- a/charts/portal/values-production.yaml +++ b/charts/portal/values-production.yaml @@ -1354,16 +1354,6 @@ intelligence: image: pullPolicy: IfNotPresent - # RBAC configuration - rbac: - create: true - - # Service Account configuration - serviceAccount: - create: true - automountServiceAccountToken: true - name: "" - # Kafka configuration for the intelligence server kafka: brokers: "kafka:9092" diff --git a/charts/portal/values.yaml b/charts/portal/values.yaml index 01d83206..43590682 100644 --- a/charts/portal/values.yaml +++ b/charts/portal/values.yaml @@ -81,12 +81,20 @@ portal: targetBootstrapServers: "kafka:9092" # Bootstrap listen port on the APIM Gateway (KafkaTcpProxy assertion) listenPort: 9192 - # Per-broker listen ports start from this value (9193, 9194, 9195, ...) - brokerStartPort: 9193 + # Per-broker listen ports start from this value (9194, 9195, 9196, ...) + # init-tssg.sh subtracts 1 for the assertion's internal brokerStartPort, + # so actual ports are (brokerStartPort-1) + nodeId where nodeId starts at 1. + brokerStartPort: 9194 # Hostname advertised to Kafka clients in metadata responses. - # Defaults to the APIM Gateway's public hostname (tssg-public-host). - # Must be externally reachable by TSSGs connecting through the proxy. + # Defaults to kafka-proxy-host (the ingress hostname). advertisedHost: "" + # Port advertised to Kafka clients. Default 443 for SNI-based routing + # through the ingress on standard HTTPS port. + advertisedPort: 443 + # Per-broker hostname pattern for SNI routing. Must contain $(nodeId). + # Defaults to kafka-proxy-broker-pattern helper (e.g., dev-portal-kafka-proxy-$(nodeId).example.com). + # Leave empty to use the auto-generated default. + brokerAddressPattern: "" # Please set analytics.replicaCount to a minimum of 2 aggregation: false # Specify a Gateway v9.x license file via set portal.license.value @@ -1202,16 +1210,6 @@ intelligence: image: pullPolicy: IfNotPresent - # RBAC configuration - rbac: - create: false - - # Service Account configuration - serviceAccount: - create: false - automountServiceAccountToken: false - name: "" - # Kafka configuration for the intelligence server kafka: brokers: "kafka:9092" @@ -1275,8 +1273,6 @@ ingress-nginx: # enabled: true # default: false # controllerValue: "k8s.io/ingress-nginx" -# tcp: -# 9443: "/dispatcher:9443" # Settings for portal jobs jobs: From 59bba8bb88b64c08098996c5a88170949f5b0453 Mon Sep 17 00:00:00 2001 From: Kiran Vaddadi Date: Mon, 16 Mar 2026 14:04:14 +0530 Subject: [PATCH 3/6] replication --- charts/kafka/Chart.yaml | 2 +- charts/kafka/templates/kafka-config.yaml | 29 ++ .../pre-scale-partition-reassignment.yaml | 279 ++++++++++++++++++ charts/kafka/values.yaml | 69 ++--- charts/portal/Chart.lock | 6 +- charts/portal/Chart.yaml | 2 +- charts/portal/charts/kafka-1.0.2.tgz | Bin 0 -> 29170 bytes charts/portal/values.yaml | 9 + 8 files changed, 342 insertions(+), 54 deletions(-) create mode 100644 charts/kafka/templates/pre-scale-partition-reassignment.yaml create mode 100644 charts/portal/charts/kafka-1.0.2.tgz diff --git a/charts/kafka/Chart.yaml b/charts/kafka/Chart.yaml index a5110f58..7180352c 100644 --- a/charts/kafka/Chart.yaml +++ b/charts/kafka/Chart.yaml @@ -7,5 +7,5 @@ maintainers: - name: Gazza7205 sources: - https://github.com/CAAPIM/apim-charts -version: 1.0.1 +version: 1.0.2 appVersion: "4.0.0" diff --git a/charts/kafka/templates/kafka-config.yaml b/charts/kafka/templates/kafka-config.yaml index ad8dd41e..694e52fb 100644 --- a/charts/kafka/templates/kafka-config.yaml +++ b/charts/kafka/templates/kafka-config.yaml @@ -129,6 +129,35 @@ data: # ============================================ KAFKA_CFG_LOG_RETENTION_HOURS: {{ .Values.kafka.logRetentionHours | quote }} + # ============================================ + # Topic Replication Configuration + # ============================================ + # Auto-calculate replication factors: use configured value, or min(replicaCount, 3) when 0 + {{- $replicaCount := int .Values.kafka.replicaCount }} + {{- $maxReplication := 3 }} + {{- $autoRF := min $replicaCount $maxReplication }} + + {{- $defaultRF := int .Values.kafka.defaultReplicationFactor }} + {{- if eq $defaultRF 0 }} + {{- $defaultRF = $autoRF }} + {{- end }} + KAFKA_CFG_DEFAULT_REPLICATION_FACTOR: {{ $defaultRF | quote }} + + KAFKA_CFG_MIN_INSYNC_REPLICAS: {{ .Values.kafka.minInsyncReplicas | quote }} + + {{- $offsetsRF := int .Values.kafka.offsetsTopicReplicationFactor }} + {{- if eq $offsetsRF 0 }} + {{- $offsetsRF = $autoRF }} + {{- end }} + KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR: {{ $offsetsRF | quote }} + + {{- $txnRF := int .Values.kafka.transactionStateLogReplicationFactor }} + {{- if eq $txnRF 0 }} + {{- $txnRF = $autoRF }} + {{- end }} + KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: {{ $txnRF | quote }} + KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR: {{ .Values.kafka.transactionStateLogMinIsr | quote }} + # ============================================ # SASL Authentication Configuration # ============================================ diff --git a/charts/kafka/templates/pre-scale-partition-reassignment.yaml b/charts/kafka/templates/pre-scale-partition-reassignment.yaml new file mode 100644 index 00000000..d4d189dc --- /dev/null +++ b/charts/kafka/templates/pre-scale-partition-reassignment.yaml @@ -0,0 +1,279 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. +# AI assistance has been used to generate some or all contents of this file. That includes, but is not limited to, new code, modifying existing code, stylistic edits. +{{- if .Values.kafka.kraft.enabled }} +# +# Pre-Upgrade Partition Reassignment Job +# +# Purpose: +# Runs BEFORE each Helm upgrade to detect Kafka broker scale-down and reassign +# topic partitions to the surviving brokers. Without this, partitions whose +# replicas reside only on removed brokers become leaderless and unavailable. +# +# How It Works: +# 1. Reads the current StatefulSet replica count from K8s +# 2. Compares it with the new desired replica count from Helm values +# 3. If scaling DOWN, connects to a surviving broker and: +# a. Lists all topic partitions +# b. Identifies partitions with replicas on brokers being removed +# c. Generates a reassignment plan moving them to surviving brokers +# d. Executes the reassignment and waits for completion +# 4. If NOT scaling down, exits immediately (no-op) +# +# Safety: +# - Only runs on scale-down (current > desired replicas) +# - Waits for ISR sync before exiting so brokers can be safely terminated +# - Uses Kafka's built-in reassignment tooling +# - Idempotent: safe to run multiple times +# +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "kafka.fullname" . }}-partition-reassign + namespace: {{ .Release.Namespace }} + labels: + {{- include "kafka.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-upgrade + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "kafka.fullname" . }}-partition-reassign + namespace: {{ .Release.Namespace }} + labels: + {{- include "kafka.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-upgrade + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +rules: +- apiGroups: ["apps"] + resources: ["statefulsets"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "kafka.fullname" . }}-partition-reassign + namespace: {{ .Release.Namespace }} + labels: + {{- include "kafka.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-upgrade + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "kafka.fullname" . }}-partition-reassign +subjects: +- kind: ServiceAccount + name: {{ include "kafka.fullname" . }}-partition-reassign + namespace: {{ .Release.Namespace }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "kafka.fullname" . }}-partition-reassign-{{ .Release.Revision }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "kafka.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-upgrade + "helm.sh/hook-weight": "-8" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + backoffLimit: 2 + activeDeadlineSeconds: 600 + template: + metadata: + labels: + {{- include "kafka.labels" . | nindent 8 }} + job: partition-reassign + spec: + serviceAccountName: {{ include "kafka.fullname" . }}-partition-reassign + restartPolicy: Never + {{- if .Values.global.pullSecret }} + imagePullSecrets: + - name: "{{ .Values.global.pullSecret }}" + {{- end }} + containers: + - name: reassign + image: "{{ .Values.global.portalRepository }}{{ .Values.image.kafka }}" + imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }} + env: + - name: KAFKA_HOME + value: /opt/ca/kafka + command: + - /bin/sh + - -c + - | + set -e + echo "=== Kafka Pre-Scale Partition Reassignment ===" + + NAMESPACE="{{ .Release.Namespace }}" + STS_NAME="{{ include "kafka.statefulsetName" . }}" + SERVICE_NAME="{{ include "kafka.fullname" . }}" + DESIRED_REPLICAS={{ int .Values.kafka.replicaCount }} + KAFKA_BIN="$KAFKA_HOME/bin" + KAFKA_PORT={{ int .Values.kafka.listeners.internal.port }} + {{- $startOrdinal := 0 }} + {{- if .Values.kafka.ordinals }} + {{- $startOrdinal = int .Values.kafka.ordinals.start }} + {{- end }} + ORDINAL_START={{ $startOrdinal }} + + echo "Desired replicas: $DESIRED_REPLICAS (ordinal start: $ORDINAL_START)" + + # Detect current replica count from the running StatefulSet + if ! command -v kubectl >/dev/null 2>&1; then + echo "kubectl not available in this image. Checking if scale-down is needed via Kafka metadata..." + + # Connect to the first surviving broker to discover current brokers + BOOTSTRAP="${STS_NAME}-${ORDINAL_START}.${SERVICE_NAME}:${KAFKA_PORT}" + echo "Connecting to bootstrap: $BOOTSTRAP" + + # Get broker list from Kafka metadata + BROKER_IDS=$($KAFKA_BIN/kafka-broker-api-versions.sh --bootstrap-server "$BOOTSTRAP" 2>/dev/null \ + | grep "^${STS_NAME}-" | sed "s/${STS_NAME}-//" | cut -d'.' -f1 | sort -n) + + if [ -z "$BROKER_IDS" ]; then + echo "Could not discover brokers. Kafka may not be running yet. Skipping reassignment." + exit 0 + fi + + CURRENT_BROKER_COUNT=$(echo "$BROKER_IDS" | wc -l | tr -d ' ') + echo "Current brokers ($CURRENT_BROKER_COUNT): $(echo $BROKER_IDS | tr '\n' ' ')" + else + # Use kubectl to get current replica count + CURRENT_REPLICAS=$(kubectl get statefulset "$STS_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "0") + + if [ "$CURRENT_REPLICAS" = "0" ] || [ -z "$CURRENT_REPLICAS" ]; then + echo "StatefulSet not found or has 0 replicas. Fresh install. Skipping." + exit 0 + fi + + echo "Current replicas: $CURRENT_REPLICAS" + CURRENT_BROKER_COUNT=$CURRENT_REPLICAS + fi + + if [ "$DESIRED_REPLICAS" -ge "$CURRENT_BROKER_COUNT" ]; then + echo "Not scaling down ($CURRENT_BROKER_COUNT -> $DESIRED_REPLICAS). No partition reassignment needed." + exit 0 + fi + + echo "=== SCALE-DOWN DETECTED: $CURRENT_BROKER_COUNT -> $DESIRED_REPLICAS ===" + + # Build the list of surviving broker IDs + SURVIVING_BROKERS="" + i=0 + while [ $i -lt $DESIRED_REPLICAS ]; do + BID=$((ORDINAL_START + i)) + if [ -z "$SURVIVING_BROKERS" ]; then + SURVIVING_BROKERS="$BID" + else + SURVIVING_BROKERS="$SURVIVING_BROKERS,$BID" + fi + i=$((i + 1)) + done + echo "Surviving broker IDs: $SURVIVING_BROKERS" + + # Build the list of brokers being removed + REMOVED_BROKERS="" + i=$DESIRED_REPLICAS + while [ $i -lt $CURRENT_BROKER_COUNT ]; do + BID=$((ORDINAL_START + i)) + if [ -z "$REMOVED_BROKERS" ]; then + REMOVED_BROKERS="$BID" + else + REMOVED_BROKERS="$REMOVED_BROKERS,$BID" + fi + i=$((i + 1)) + done + echo "Brokers being removed: $REMOVED_BROKERS" + + BOOTSTRAP="${STS_NAME}-${ORDINAL_START}.${SERVICE_NAME}:${KAFKA_PORT}" + echo "Bootstrap server: $BOOTSTRAP" + + # Use the Kafka reassign tool's --generate mode with a topics JSON + TOPICS=$($KAFKA_BIN/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" --list 2>/dev/null | grep -v "^__") + + if [ -z "$TOPICS" ]; then + echo "No user topics found. Skipping reassignment." + exit 0 + fi + + echo "Topics to check: $(echo $TOPICS | tr '\n' ' ')" + + # Build topics-to-move JSON + TOPICS_JSON='{"version":1,"topics":[' + TFIRST=true + for TOPIC in $TOPICS; do + if [ "$TFIRST" = "true" ]; then + TFIRST=false + else + TOPICS_JSON="${TOPICS_JSON}," + fi + TOPICS_JSON="${TOPICS_JSON}{\"topic\":\"$TOPIC\"}" + done + TOPICS_JSON="${TOPICS_JSON}]}" + + echo "$TOPICS_JSON" > /tmp/topics-to-move.json + + # Generate the reassignment plan targeting surviving brokers only + echo "Generating reassignment plan for brokers: $SURVIVING_BROKERS" + GENERATED=$($KAFKA_BIN/kafka-reassign-partitions.sh \ + --bootstrap-server "$BOOTSTRAP" \ + --generate \ + --topics-to-move-json-file /tmp/topics-to-move.json \ + --broker-list "$SURVIVING_BROKERS" 2>&1) + + echo "$GENERATED" + + # Extract the proposed reassignment (second JSON block in output) + PROPOSED=$(echo "$GENERATED" | grep "^{" | tail -1) + + if [ -z "$PROPOSED" ]; then + echo "No reassignment plan generated. Skipping." + exit 0 + fi + + echo "$PROPOSED" > /tmp/reassignment.json + echo "=== Executing partition reassignment ===" + cat /tmp/reassignment.json + + $KAFKA_BIN/kafka-reassign-partitions.sh \ + --bootstrap-server "$BOOTSTRAP" \ + --execute \ + --reassignment-json-file /tmp/reassignment.json + + echo "=== Waiting for reassignment to complete ===" + TIMEOUT=300 + ELAPSED=0 + while [ $ELAPSED -lt $TIMEOUT ]; do + STATUS=$($KAFKA_BIN/kafka-reassign-partitions.sh \ + --bootstrap-server "$BOOTSTRAP" \ + --verify \ + --reassignment-json-file /tmp/reassignment.json 2>&1) + + echo "$STATUS" | tail -5 + + if echo "$STATUS" | grep -q "is complete"; then + if ! echo "$STATUS" | grep -q "still in progress"; then + echo "=== All partition reassignments completed ===" + exit 0 + fi + fi + + sleep 5 + ELAPSED=$((ELAPSED + 5)) + echo "Waiting... ($ELAPSED/${TIMEOUT}s)" + done + + echo "WARNING: Reassignment did not complete within ${TIMEOUT}s. Proceeding anyway." + echo "Partitions may need manual reassignment after scale-down." + exit 0 +{{- end }} diff --git a/charts/kafka/values.yaml b/charts/kafka/values.yaml index bf197ab5..7050e80a 100644 --- a/charts/kafka/values.yaml +++ b/charts/kafka/values.yaml @@ -1,4 +1,3 @@ -# Global values (inherited from parent chart) global: portalRepository: "" storageClass: "" @@ -61,6 +60,7 @@ kafka: # limits: # cpu: 1000m # memory: 1.5Gi + # KRaft Mode Configuration (Kafka 4.0.0+) kraft: # Enable KRaft mode (Zookeeper-less) @@ -76,6 +76,7 @@ kafka: # Controller quorum voters format: id@host:port # For StatefulSet with 3 replicas: "0@kafka-0.kafka:9093,1@kafka-1.kafka:9093,2@kafka-2.kafka:9093" controllerQuorumVoters: "" + # Listener Configuration listeners: # Internal listener for inter-broker and client communication within cluster @@ -90,6 +91,7 @@ kafka: protocol: PLAINTEXT # Inter-broker listener name interBrokerListenerName: INTERNAL + # Advertised Listeners Configuration # Custom advertised listeners (portal-dist style) # Format: "INTERNAL://kafka:9092" @@ -99,53 +101,22 @@ kafka: # Log retention configuration logRetentionHours: 6 -<<<<<<< HEAD - # TLS/SSL Configuration - tls: - # Enable TLS/SSL (required when external listener uses SSL protocol) - enabled: false - # TLS type (PEM or JKS) - type: PEM - # Client authentication (none, requested, required) - clientAuth: required - # ============================================ - # Option 1: Kubernetes Secret (Recommended) - # ============================================ - # Secret containing TLS certificates - # Should contain: keystore.key, truststore.pem - # When using Intelligence, set to portal-internal-secret - secretName: "" - # Keys in the secret - keystoreKeyKey: "keystore.key" - truststoreCertKey: "truststore.pem" - # Password secret for encrypted keys (required for encrypted private keys) - # When using Intelligence, set to portal-internal-secret - passwordSecretName: "" - passwordSecretKey: "keypass.txt" - # TSSG Certificates (use apim-ssl from internal secret by default) - # The secret name for TSSG certificates (apim-ssl) - # Changed to portal-external-secret to support static certificate deployments - internalSecretName: "portal-external-secret" - tssgKeyKey: "apim-ssl.p8.key" # Default to apim-ssl, can be overridden - tssgCertKey: "apim-ssl.crt" # Default to apim-ssl, can be overridden - passwordKey: "keypass.txt" - # ============================================ - # Option 2: Inline Certificates (portal-dist style) - # ============================================ - # Pass certificates as environment variables - # These take precedence over secretName if both are set - # Can be base64 encoded or PEM format - truststoreCertContent: "" - keystoreKeyContent: "" - # ============================================ - # Option 3: CA Certificates (analytics_util style) - # ============================================ - # Alternative certificate format - caKey: "" - caCertificate: "" - caTruststoreCertificate: "" -======= ->>>>>>> 44b9d09 (remove kafka tls) + # Topic replication configuration + # default.replication.factor: applied to auto-created topics + # Set to min(replicaCount, 3) by default for resilience during scale-down + # When set to 0 (default), the chart auto-calculates: min(replicaCount, 3) + defaultReplicationFactor: 0 + # min.insync.replicas: minimum replicas that must acknowledge a write + # Must be <= replicaCount. Set to 1 to allow single-broker operation. + minInsyncReplicas: 1 + # offsets.topic.replication.factor: replication for __consumer_offsets + # When set to 0 (default), the chart auto-calculates: min(replicaCount, 3) + offsetsTopicReplicationFactor: 0 + # transaction.state.log.replication.factor + transactionStateLogReplicationFactor: 0 + # transaction.state.log.min.isr + transactionStateLogMinIsr: 1 + # SASL Authentication Configuration sasl: # Enable SASL authentication @@ -184,4 +155,4 @@ serviceAccount: kubectlImage: repository: "tls-automator" tag: "latest" - pullPolicy: IfNotPresent + pullPolicy: IfNotPresent \ No newline at end of file diff --git a/charts/portal/Chart.lock b/charts/portal/Chart.lock index 1bd46c16..865d7131 100644 --- a/charts/portal/Chart.lock +++ b/charts/portal/Chart.lock @@ -10,9 +10,9 @@ dependencies: version: 1.0.4 - name: kafka repository: file://../kafka - version: 1.0.1 + version: 1.0.2 - name: ingress-nginx repository: https://kubernetes.github.io/ingress-nginx/ version: 4.12.1 -digest: sha256:a3d25593eb6299015d8e8b99970bff8858a3d8cb71bb4bae34829995e9d20ecf -generated: "2026-03-13T11:18:42.831349+05:30" +digest: sha256:fb4f9aa9bdde4b986cfd0c8fd56e60b8432e79994254d1720a8da9a093739bb1 +generated: "2026-03-16T12:59:25.278734+05:30" diff --git a/charts/portal/Chart.yaml b/charts/portal/Chart.yaml index 45c2dde0..2a081e66 100644 --- a/charts/portal/Chart.yaml +++ b/charts/portal/Chart.yaml @@ -22,7 +22,7 @@ dependencies: condition: portal.analytics.enabled repository: "file://../seaweedfs" - name: kafka - version: ^1.0.1 + version: ^1.0.2 condition: kafka.enabled repository: "file://../kafka" - name: ingress-nginx diff --git a/charts/portal/charts/kafka-1.0.2.tgz b/charts/portal/charts/kafka-1.0.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..aadb0a09fa5cd9c1f722d9f9db979c1ad5419ab6 GIT binary patch literal 29170 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 N|Nk*Squ&5V0RW$%5%T~5 literal 0 HcmV?d00001 diff --git a/charts/portal/values.yaml b/charts/portal/values.yaml index 43590682..0b5f911f 100644 --- a/charts/portal/values.yaml +++ b/charts/portal/values.yaml @@ -1091,6 +1091,15 @@ kafka: # Log retention configuration logRetentionHours: 6 + # Topic replication configuration + # 0 = auto-calculate as min(replicaCount, 3) + defaultReplicationFactor: 0 + # Must be <= replicaCount; set to 1 for single-broker tolerance + minInsyncReplicas: 1 + offsetsTopicReplicationFactor: 0 + transactionStateLogReplicationFactor: 0 + transactionStateLogMinIsr: 1 + # SASL Authentication Configuration sasl: # Enable SASL authentication From 3e7b38aacc41a01d4f3d9ebda202eaa189355b1c Mon Sep 17 00:00:00 2001 From: Kiran Vaddadi Date: Mon, 16 Mar 2026 14:29:45 +0530 Subject: [PATCH 4/6] remove intelligence sub-chart lint check --- .github/ct-lint.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ct-lint.yaml b/.github/ct-lint.yaml index cd631fa1..45d70eb6 100644 --- a/.github/ct-lint.yaml +++ b/.github/ct-lint.yaml @@ -4,7 +4,6 @@ charts: - charts/gateway - charts/seaweedfs - charts/kafka - - charts/intelligence chart-repos: - hazelcast=https://hazelcast-charts.s3.amazonaws.com/ - nginx=https://kubernetes.github.io/ingress-nginx/ From 7a158e9d1f7f94879f03fd875f835de110238d2e Mon Sep 17 00:00:00 2001 From: Kiran Saladi Date: Tue, 26 May 2026 10:48:02 +0000 Subject: [PATCH 5/6] [charts/portal] Change to make Kafka as default for deployments, intelligence --- charts/portal/README.md | 30 +++++++---- charts/portal/charts/kafka-1.0.0.tgz | Bin 18252 -> 0 bytes charts/portal/templates/_helpers.tpl | 23 ++++++--- .../analytics-server/analytics-config.yaml | 2 + charts/portal/templates/apim/apim-config.yaml | 19 +++---- .../portal/templates/apim/apim-service.yaml | 10 ++-- .../portal/templates/gateway-api/gateway.yaml | 2 - .../templates/gateway-api/tlsroute.yaml | 6 +-- .../templates/ingress/contour-httpproxy.yaml | 6 +-- charts/portal/templates/ingress/ingress.yaml | 2 - charts/portal/templates/ingress/route.yaml | 2 - .../intelligence/intelligence-config.yaml | 3 +- .../intelligence/intelligence-deployment.yaml | 4 +- .../portal-data/portal-data-config.yaml | 6 ++- charts/portal/values-production.yaml | 25 +++++---- charts/portal/values.yaml | 48 +++++++++++------- 16 files changed, 108 insertions(+), 80 deletions(-) delete mode 100644 charts/portal/charts/kafka-1.0.0.tgz diff --git a/charts/portal/README.md b/charts/portal/README.md index c970a3c6..aa8055bc 100644 --- a/charts/portal/README.md +++ b/charts/portal/README.md @@ -828,8 +828,8 @@ For Production, use an external MySQL Server. The Intelligence server provides advanced analytics and third-party agent integration capabilities. **Important Dependencies:** -- When `portal.intelligence.enabled: true`, the following subcharts are automatically deployed: - - **Kafka**: Required for message streaming (should be configured with 3+ replicas for production) +- Kafka is always deployed - intelligence consumes from the same in-cluster Kafka used by Portal deployment messaging, analytics. +- For production with intelligence enabled, set `kafka.kafka.replicaCount: 3`. | Parameter | Description | Default | | --- | --- | --- | @@ -837,6 +837,19 @@ The Intelligence server provides advanced analytics and third-party agent integr | `intelligence.replicaCount` | Number of intelligence server replicas | `1` | | `intelligence.kafka.kafkaCa.caSecretName` | Secret containing the CA certificate for Kafka mTLS | `portal-external-secret` | +## Kafka and KafkaTcpProxy on APIM +Portal 5.4.2 (rmq2kafka / F161288) uses Kafka for deployment messaging. Kafka is always deployed; the brokers' internal listener (9092) is never exposed externally. All external Kafka traffic is fronted by the `KafkaTcpProxyAssertion` on the APIM gateway, with SNI-based per-broker routing on port 443. + +| Parameter | Description | Default | +| --- | --- | --- | +| `portal.kafka.proxy.targetBootstrapServers` | Upstream Kafka the proxy forwards to | `kafka:9092` | +| `portal.kafka.proxy.listenPort` | APIM bootstrap listen port for Kafka clients | `9191` | +| `portal.kafka.proxy.brokerStartPort` | Per-broker start port; per-broker port = `brokerStartPort + nodeId` | `9193` | +| `portal.kafka.proxy.advertisedHost` | Hostname advertised to Kafka clients (defaults to `kafka-proxy-host` helper) | `""` | +| `portal.kafka.proxy.advertisedPort` | Port advertised to Kafka clients; `443` enables SNI routing through ingress | `443` | +| `portal.kafka.proxy.brokerAddressPattern` | Per-broker SNI hostname pattern with `$(nodeId)` (defaults to helper) | `""` | +| `portal.kafka.proxy.configBrokerHost` | Value baked into the enrollment bundle CWP `portal.config.broker.kafka.host` | `localhost` | + ## SeaweedFS The SeaweedFS subchart provides S3-compatible object storage for analytics data. @@ -858,18 +871,15 @@ The SeaweedFS subchart provides S3-compatible object storage for analytics data. For detailed SeaweedFS configuration and migration guide, see [Minio to SeaweedFS Migration Guide](../../utils/MINIO-TO-SEAWEEDFS-MIGRATION.md). -## Kafka -The Kafka subchart provides Apache Kafka 4.0.0 in KRaft mode (Zookeeper-less) for message streaming. - -**Automatic Deployment:** -- Deployed when `kafka.enabled: true` -- Required for Intelligence service +## Kafka Subchart +The Kafka subchart provides Apache Kafka 4.0.0 in KRaft mode (Zookeeper-less). Kafka is the messaging backbone for Portal. | Parameter | Description | Default | | --- | --- | --- | -| `kafka.enabled` | Enable Kafka subchart | `false` | -| `kafka.kafka.replicaCount` | Number of Kafka broker replicas (3+ recommended for production) | `1` | +| `kafka.enabled` | Deploy the Kafka subchart | `true` | +| `kafka.kafka.replicaCount` | Number of Kafka broker replicas (3+ recommended for production with intelligence) | `1` | | `kafka.kafka.kraft.enabled` | Enable KRaft mode (Zookeeper-less) | `true` | +| `kafka.kafka.ordinals.start` | StatefulSet pod start ordinal (1 preserves KRaft nodeId across Zookeeper upgrade) | `1` | For detailed Kafka configuration, see the [Kafka Chart README](../kafka/README.md). diff --git a/charts/portal/charts/kafka-1.0.0.tgz b/charts/portal/charts/kafka-1.0.0.tgz deleted file mode 100644 index d8242c369b3d3da605c4e319e827f297f1978dd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18252 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 P00960B2`j$08jw{nA~Pa diff --git a/charts/portal/templates/_helpers.tpl b/charts/portal/templates/_helpers.tpl index a6048e38..f023580d 100644 --- a/charts/portal/templates/_helpers.tpl +++ b/charts/portal/templates/_helpers.tpl @@ -486,10 +486,18 @@ Get "intelligence" database name {{- end -}} {{/* -Get Kafka broker address for intelligence server. -Uses the kafka subchart's fullnameOverride and internal listener port. + ============================================================ + Kafka Helpers (shared by portal-data, analytics-server, + tenant-provisioner, authenticator, intelligence) + ============================================================ +*/}} + +{{/* +In-cluster Kafka bootstrap broker list. Resolves to the kafka subchart's +service name + internal listener port (default kafka:9092). Used by every +in-cluster Kafka producer / consumer */}} -{{- define "intelligence.kafkaBrokers" -}} +{{- define "kafka.brokers" -}} {{- $kafkaName := default "kafka" .Values.kafka.fullnameOverride -}} {{- if and .Values.kafka.kafka .Values.kafka.kafka.listeners }} {{- printf "%s:%g" $kafkaName .Values.kafka.kafka.listeners.internal.port -}} @@ -499,11 +507,12 @@ Uses the kafka subchart's fullnameOverride and internal listener port. {{- end -}} {{/* -Generate Kafka proxy bootstrap address for intelligence server. -Uses the advertised port (default 443) for SNI-based routing. +External Kafka proxy bootstrap address advertised to tenant Gateways via SNI +on port 443 (default). Used by intelligence server and exposed via APIM +KafkaTcpProxy assertion. */}} -{{- define "intelligence.kafkaProxyBootstrap" -}} +{{- define "kafka.proxyBootstrap" -}} {{- $host := include "kafka-proxy-host" . -}} - {{- $port := int (.Values.portal.intelligence.kafkaProxy.advertisedPort | default 443) -}} + {{- $port := int (.Values.portal.kafka.proxy.advertisedPort | default 443) -}} {{- printf "%s:%d" $host $port -}} {{- end -}} diff --git a/charts/portal/templates/analytics-server/analytics-config.yaml b/charts/portal/templates/analytics-server/analytics-config.yaml index 420afe17..ed93f999 100644 --- a/charts/portal/templates/analytics-server/analytics-config.yaml +++ b/charts/portal/templates/analytics-server/analytics-config.yaml @@ -1,3 +1,4 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. apiVersion: v1 kind: ConfigMap metadata: @@ -27,6 +28,7 @@ data: PORTAL_VERSION: {{ .Chart.AppVersion | quote }} RABBITMQ_HOST: {{ .Values.rabbitmq.host | quote }} RABBITMQ_PORT: {{ .Values.rabbitmq.service.port | quote }} + KAFKA_BROKERS: {{ include "kafka.brokers" . | quote }} PAPI_PUBLIC_HOST: {{ include "tssg-public-host" . | quote }} PAPI_INGRESS_TENANT: {{ include "default-tenant-id" . | quote }} {{ if .Values.analytics.additionalEnv }} diff --git a/charts/portal/templates/apim/apim-config.yaml b/charts/portal/templates/apim/apim-config.yaml index 454f945a..8986a656 100644 --- a/charts/portal/templates/apim/apim-config.yaml +++ b/charts/portal/templates/apim/apim-config.yaml @@ -30,17 +30,14 @@ data: TENANT_ID: {{ include "default-tenant-id" . | quote }} TSSG_PUBLIC_HOST: {{ include "tssg-public-host" . | quote }} TSSG_PUBLIC_PORT: {{ .Values.portal.otk.port | quote }} -{{- if .Values.portal.intelligence.enabled }} - INTELLIGENCE_ENABLED: "true" - KAFKA_PROXY_TARGET_SERVERS: {{ default "kafka:9092" .Values.portal.intelligence.kafkaProxy.targetBootstrapServers | quote }} - KAFKA_PROXY_LISTEN_PORT: {{ default 9192 .Values.portal.intelligence.kafkaProxy.listenPort | quote }} - KAFKA_PROXY_BROKER_START_PORT: {{ default 9194 .Values.portal.intelligence.kafkaProxy.brokerStartPort | quote }} - KAFKA_PROXY_ADVERTISED_HOST: {{ default (include "kafka-proxy-host" .) .Values.portal.intelligence.kafkaProxy.advertisedHost | quote }} - KAFKA_PROXY_ADVERTISED_PORT: {{ default 443 .Values.portal.intelligence.kafkaProxy.advertisedPort | quote }} - KAFKA_PROXY_BROKER_ADDRESS_PATTERN: {{ default (include "kafka-proxy-broker-pattern" .) .Values.portal.intelligence.kafkaProxy.brokerAddressPattern | quote }} -{{- else }} - INTELLIGENCE_ENABLED: "false" -{{- end }} + INTELLIGENCE_ENABLED: {{ .Values.portal.intelligence.enabled | default false | quote }} + KAFKA_PROXY_TARGET_SERVERS: {{ default "kafka:9092" .Values.portal.kafka.proxy.targetBootstrapServers | quote }} + KAFKA_PROXY_LISTEN_PORT: {{ default 9191 .Values.portal.kafka.proxy.listenPort | quote }} + KAFKA_PROXY_BROKER_START_PORT: {{ default 9193 .Values.portal.kafka.proxy.brokerStartPort | quote }} + KAFKA_PROXY_ADVERTISED_HOST: {{ default (include "kafka-proxy-host" .) .Values.portal.kafka.proxy.advertisedHost | quote }} + KAFKA_PROXY_ADVERTISED_PORT: {{ default 443 .Values.portal.kafka.proxy.advertisedPort | quote }} + KAFKA_PROXY_BROKER_ADDRESS_PATTERN: {{ default (include "kafka-proxy-broker-pattern" .) .Values.portal.kafka.proxy.brokerAddressPattern | quote }} + PORTAL_CONFIG_BROKER_KAFKA_HOST: {{ default "localhost" .Values.portal.kafka.proxy.configBrokerHost | quote }} {{ if .Values.apim.additionalEnv }} {{- range $key, $val := .Values.apim.additionalEnv }} {{ $key }}: {{ $val | quote }} diff --git a/charts/portal/templates/apim/apim-service.yaml b/charts/portal/templates/apim/apim-service.yaml index 5912120c..35e0ed33 100644 --- a/charts/portal/templates/apim/apim-service.yaml +++ b/charts/portal/templates/apim/apim-service.yaml @@ -42,12 +42,11 @@ spec: targetPort: 9448 protocol: TCP name: apim-sso -{{- if .Values.portal.intelligence.enabled }} - - port: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} - targetPort: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} + - port: {{ .Values.portal.kafka.proxy.listenPort | default 9191 }} + targetPort: {{ .Values.portal.kafka.proxy.listenPort | default 9191 }} protocol: TCP name: kfk-proxy-boot -{{- $brokerStartPort := int (.Values.portal.intelligence.kafkaProxy.brokerStartPort | default 9194) }} +{{- $brokerStartPort := int (.Values.portal.kafka.proxy.brokerStartPort | default 9193) }} {{- $replicaCount := int (.Values.kafka.kafka.replicaCount | default 1) }} {{- $ordinalStart := 0 }} {{- if and .Values.kafka.kafka.ordinals (hasKey .Values.kafka.kafka.ordinals "start") }} @@ -55,12 +54,11 @@ spec: {{- end }} {{- range $i := until $replicaCount }} {{- $nodeId := add $ordinalStart $i }} -{{- $brokerPort := add (sub $brokerStartPort 1) $nodeId }} +{{- $brokerPort := add $brokerStartPort $nodeId }} - port: {{ $brokerPort }} targetPort: {{ $brokerPort }} protocol: TCP name: kfk-proxy-brk-{{ $nodeId }} -{{- end }} {{- end }} selector: app: apim diff --git a/charts/portal/templates/gateway-api/gateway.yaml b/charts/portal/templates/gateway-api/gateway.yaml index 4ec521a8..98d9544a 100644 --- a/charts/portal/templates/gateway-api/gateway.yaml +++ b/charts/portal/templates/gateway-api/gateway.yaml @@ -42,7 +42,6 @@ spec: {{- $hostnames = append $hostnames (include "pssg-sso-host" .) }} {{- $hostnames = append $hostnames (include "broker-host" .) }} {{/* Kafka proxy routes: bootstrap + per-broker SNI hostnames */}} - {{- if .Values.portal.intelligence.enabled }} {{- $hostnames = append $hostnames (include "kafka-proxy-host" .) }} {{- $replicaCount := int (.Values.kafka.kafka.replicaCount | default 1) }} {{- $ordinalStart := 0 }} @@ -53,7 +52,6 @@ spec: {{- $nodeId := add $ordinalStart $i }} {{- $hostnames = append $hostnames (include "kafka-proxy-broker-host" (dict "root" $ "nodeId" $nodeId)) }} {{- end }} - {{- end }} {{/* Dynamic tenant routes */}} {{- range .Values.ingress.tenantIds }} {{- $hostnames = append $hostnames (printf "%s.%s" . $.Values.portal.domain) }} diff --git a/charts/portal/templates/gateway-api/tlsroute.yaml b/charts/portal/templates/gateway-api/tlsroute.yaml index 3fbb9e20..8636fd29 100644 --- a/charts/portal/templates/gateway-api/tlsroute.yaml +++ b/charts/portal/templates/gateway-api/tlsroute.yaml @@ -232,7 +232,6 @@ spec: - backendRefs: - name: apim port: 1885 -{{- if .Values.portal.intelligence.enabled }} --- apiVersion: {{ include "portal.gatewayAPI.tlsRouteApiVersion" $root }} kind: TLSRoute @@ -264,8 +263,8 @@ spec: rules: - backendRefs: - name: apim - port: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} -{{- $brokerStartPort := int (.Values.portal.intelligence.kafkaProxy.brokerStartPort | default 9193) }} + port: {{ .Values.portal.kafka.proxy.listenPort | default 9191 }} +{{- $brokerStartPort := int (.Values.portal.kafka.proxy.brokerStartPort | default 9193) }} {{- $replicaCount := int (.Values.kafka.kafka.replicaCount | default 1) }} {{- $ordinalStart := 0 }} {{- if and .Values.kafka.kafka.ordinals (hasKey .Values.kafka.kafka.ordinals "start") }} @@ -307,7 +306,6 @@ spec: - name: apim port: {{ $brokerPort }} {{- end }} -{{- end }} {{/* ============================================================ Dynamic TLSRoutes - generated from ingress.tenantIds diff --git a/charts/portal/templates/ingress/contour-httpproxy.yaml b/charts/portal/templates/ingress/contour-httpproxy.yaml index 9d2bab6a..18791aee 100644 --- a/charts/portal/templates/ingress/contour-httpproxy.yaml +++ b/charts/portal/templates/ingress/contour-httpproxy.yaml @@ -176,7 +176,6 @@ spec: port: 1885 --- -{{- if .Values.portal.intelligence.enabled }} --- apiVersion: projectcontour.io/v1 kind: HTTPProxy @@ -201,8 +200,8 @@ spec: tcpproxy: services: - name: apim - port: {{ .Values.portal.intelligence.kafkaProxy.listenPort | default 9192 }} -{{- $brokerStartPort := int (.Values.portal.intelligence.kafkaProxy.brokerStartPort | default 9193) }} + port: {{ .Values.portal.kafka.proxy.listenPort | default 9191 }} +{{- $brokerStartPort := int (.Values.portal.kafka.proxy.brokerStartPort | default 9193) }} {{- $replicaCount := int (.Values.kafka.kafka.replicaCount | default 1) }} {{- $ordinalStart := 0 }} {{- if and .Values.kafka.kafka.ordinals (hasKey .Values.kafka.kafka.ordinals "start") }} @@ -237,7 +236,6 @@ spec: - name: apim port: {{ $brokerPort }} {{- end }} -{{- end }} {{- range $i,$tenantId := $.Values.ingress.tenantIds }} --- diff --git a/charts/portal/templates/ingress/ingress.yaml b/charts/portal/templates/ingress/ingress.yaml index e471a6e5..976da54e 100644 --- a/charts/portal/templates/ingress/ingress.yaml +++ b/charts/portal/templates/ingress/ingress.yaml @@ -147,7 +147,6 @@ spec: serviceName: apim servicePort: {{ printf "%s-broker" .Values.portal.defaultTenantId | quote }} {{- end }} -{{- if .Values.portal.intelligence.enabled }} - host: {{ include "kafka-proxy-host" . | quote }} http: paths: @@ -188,7 +187,6 @@ spec: servicePort: kfk-proxy-brk-{{ $nodeId }} {{- end }} {{- end }} -{{- end }} {{- range .Values.ingress.customRoutes }} - host: "{{ .subdomain }}.{{ $.Values.portal.domain }}" http: diff --git a/charts/portal/templates/ingress/route.yaml b/charts/portal/templates/ingress/route.yaml index 08c00e43..8741db2c 100644 --- a/charts/portal/templates/ingress/route.yaml +++ b/charts/portal/templates/ingress/route.yaml @@ -126,7 +126,6 @@ spec: name: apim weight: 100 wildcardPolicy: None -{{- if .Values.portal.intelligence.enabled }} --- apiVersion: route.openshift.io/v1 kind: Route @@ -172,4 +171,3 @@ spec: wildcardPolicy: None {{- end }} {{- end }} -{{- end }} diff --git a/charts/portal/templates/intelligence/intelligence-config.yaml b/charts/portal/templates/intelligence/intelligence-config.yaml index 29f21a7e..24d96b19 100644 --- a/charts/portal/templates/intelligence/intelligence-config.yaml +++ b/charts/portal/templates/intelligence/intelligence-config.yaml @@ -1,3 +1,4 @@ +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. {{- if .Values.portal.intelligence.enabled }} apiVersion: v1 kind: ConfigMap @@ -18,7 +19,7 @@ data: DATABASE_USE_SSL: {{ .Values.global.databaseUseSSL | quote }} DATABASE_REQUIRE_SSL: {{ .Values.global.databaseRequireSSL | quote }} INTELLIGENCE_DATABASE_NAME: {{ include "intelligence-db-name" . | quote }} - KAFKA_BROKERS: {{ include "intelligence.kafkaBrokers" . | quote }} + KAFKA_BROKERS: {{ include "kafka.brokers" . | quote }} PORTAL_DATA_HOST: {{ .Values.intelligence.portalDataHost | default "portal-data:8080" | quote }} RABBITMQ_HOST: {{ .Values.rabbitmq.host | quote }} RABBITMQ_PORT: {{ .Values.rabbitmq.service.port | quote }} diff --git a/charts/portal/templates/intelligence/intelligence-deployment.yaml b/charts/portal/templates/intelligence/intelligence-deployment.yaml index b3688baa..c84434ca 100644 --- a/charts/portal/templates/intelligence/intelligence-deployment.yaml +++ b/charts/portal/templates/intelligence/intelligence-deployment.yaml @@ -88,7 +88,7 @@ spec: key: mysql-password {{- end }} - name: KAFKA_BROKERS - value: {{ .Values.intelligence.kafka.brokers | default "kafka:9092" | quote }} + value: {{ default (include "kafka.brokers" .) .Values.intelligence.kafka.brokers | quote }} - name: KAFKA_SECURITY_PROTOCOL value: {{ .Values.intelligence.kafka.securityProtocol | default "PLAINTEXT" | quote }} {{- if .Values.intelligence.kafka.kafkaCa.caSecretName }} @@ -127,7 +127,7 @@ spec: value: "" {{- end }} - name: KAFKA_EXTERNAL_ADVERTIZED_BROKERS - value: {{ include "intelligence.kafkaProxyBootstrap" . | quote }} + value: {{ include "kafka.proxyBootstrap" . | quote }} {{- if .Values.intelligence.env }} {{- range $key, $value := .Values.intelligence.env }} - name: {{ $key }} diff --git a/charts/portal/templates/portal-data/portal-data-config.yaml b/charts/portal/templates/portal-data/portal-data-config.yaml index a7149261..6ae48e89 100644 --- a/charts/portal/templates/portal-data/portal-data-config.yaml +++ b/charts/portal/templates/portal-data/portal-data-config.yaml @@ -1,4 +1,5 @@ -# Copyright (c) 2025 Broadcom Inc. and its subsidiaries. All Rights Reserved. +# Copyright (c) 2026 Broadcom Inc. and its subsidiaries. All Rights Reserved. +# AI assistance has been used to generate some or all contents of this file. That includes, but is not limited to, new code, modifying existing code, stylistic edits. apiVersion: v1 kind: ConfigMap metadata: @@ -35,6 +36,9 @@ data: PORTAL_VERSION: {{ .Chart.AppVersion | quote }} RABBITMQ_HOST: {{ .Values.rabbitmq.host | quote }} RABBITMQ_PORT: {{ .Values.rabbitmq.service.port | quote }} + KAFKA_BROKERS: {{ include "kafka.brokers" . | quote }} + KAFKA_PROXY_ADVERTISED_HOST: {{ default (include "kafka-proxy-host" .) .Values.portal.kafka.proxy.advertisedHost | quote }} + CONFIG_BROKER_KAFKA_PORT: {{ default 443 .Values.portal.kafka.proxy.advertisedPort | quote }} RBAC_DATABASE_NAME: {{ include "rbac-db-name" . | quote }} RBAC_DATABASE_PORT: {{ include "database-port" . | quote }} RBAC_DATABASE_TYPE: {{ required "Please fill in databaseType in values.yaml" .Values.global.databaseType | quote }} diff --git a/charts/portal/values-production.yaml b/charts/portal/values-production.yaml index dea6d359..c7ac924a 100644 --- a/charts/portal/values-production.yaml +++ b/charts/portal/values-production.yaml @@ -66,14 +66,22 @@ portal: enrollNotificationEmail: noreply@mail.example.com analytics: enabled: true - # Intelligence feature for advanced analytics and third-party agent integration - # IMPORTANT: When intelligence.enabled is true: - # - kafka.kafka.replicaCount should be set to 3 (recommended for production) + aggregation: true + # Kafka is the messaging backbone for Portal + # Production recommendation: kafka.kafka.replicaCount: 3 (see Kafka subchart block below). + kafka: + proxy: + targetBootstrapServers: "kafka:9092" + listenPort: 9191 + brokerStartPort: 9193 + advertisedHost: "" + advertisedPort: 443 + brokerAddressPattern: "" + configBrokerHost: "localhost" + # Intelligence feature for advanced analytics and third-party agent integration. # Intelligence is disabled by default. Enable it for third-party agent integration. intelligence: enabled: false - # Please set analytics.replicaCount to a minimum of 2 - aggregation: true # Specify a Gateway v9.x license file via set portal.license.value # This bootstraps a license to apim # To renew a license toggle license.secretName and specify your new license with helm upgrade @@ -1139,11 +1147,10 @@ druid: ingestion: ingestion-server:5.4.1 -# Configuration for the custom Kafka subchart +# Configuration for the custom Kafka subchart. +# Kafka is the messaging backbone for Portal and is always deployed. The kafka pod's internal listener (9092) is never exposed +# externally - all external Kafka traffic flows through the APIM Kafka Proxy assertion. kafka: - # Enable/disable Kafka deployment - # Automatically enabled when analytics or intelligence is enabled - # Set to false to explicitly disable Kafka even when analytics/intelligence are enabled enabled: true # Global configuration passed to Kafka subchart diff --git a/charts/portal/values.yaml b/charts/portal/values.yaml index 0b5f911f..162904d9 100644 --- a/charts/portal/values.yaml +++ b/charts/portal/values.yaml @@ -69,22 +69,20 @@ portal: enrollNotificationEmail: noreply@mail.example.com analytics: enabled: true - # Intelligence feature for advanced analytics and third-party agent integration - # IMPORTANT: When intelligence.enabled is true: - # - kafka.kafka.replicaCount should be set to 3 (recommended for production) - # Intelligence is disabled by default. Enable it for third-party agent integration. - intelligence: - enabled: false - # KafkaTcpProxy configuration on APIM Gateway - kafkaProxy: - # Internal Kafka bootstrap servers that the proxy forwards traffic to + + # Kafka is the messaging backbone for Portal : + # Kafka is always deployed and proxied through APIM + # the kafka subchart itself is governed by kafka.enabled. + kafka: + # Kafka Proxy configuration on APIM Gateway (server mode, SNI on :443). + proxy: + # Internal Kafka bootstrap servers that the proxy forwards traffic to. targetBootstrapServers: "kafka:9092" - # Bootstrap listen port on the APIM Gateway (KafkaTcpProxy assertion) - listenPort: 9192 - # Per-broker listen ports start from this value (9194, 9195, 9196, ...) - # init-tssg.sh subtracts 1 for the assertion's internal brokerStartPort, - # so actual ports are (brokerStartPort-1) + nodeId where nodeId starts at 1. - brokerStartPort: 9194 + # Bootstrap listen port on the APIM Gateway (KafkaTcpProxy assertion). + listenPort: 9191 + # Per-broker port = brokerStartPort + nodeId; with kafka.kafka.ordinals.start=1 + # this yields 9194, 9195, 9196 ... for KRaft brokers internally + brokerStartPort: 9193 # Hostname advertised to Kafka clients in metadata responses. # Defaults to kafka-proxy-host (the ingress hostname). advertisedHost: "" @@ -95,6 +93,18 @@ portal: # Defaults to kafka-proxy-broker-pattern helper (e.g., dev-portal-kafka-proxy-$(nodeId).example.com). # Leave empty to use the auto-generated default. brokerAddressPattern: "" + # Value baked into the enrollment bundle for the tenant Gateway CWP + # portal.config.broker.kafka.host. Production policy is "localhost" + # the tenant Gateway's Kafka client connects to its local + # Kafka Proxy relay; the relay handles upstream TLS / mTLS / SNI / + # HTTP CONNECT. The port baked into the bundle is KAFKA_PROXY_LISTEN_PORT + configBrokerHost: "localhost" + # Intelligence feature for advanced analytics and third-party agent integration + # IMPORTANT: When intelligence.enabled is true: + # - kafka.kafka.replicaCount should be set to 3 (recommended for production) + # Intelligence is disabled by default. Enable it for third-party agent integration. + intelligence: + enabled: false # Please set analytics.replicaCount to a minimum of 2 aggregation: false # Specify a Gateway v9.x license file via set portal.license.value @@ -1005,11 +1015,11 @@ druid: ingestion: ingestion-server:5.4.1 -# Configuration for the custom Kafka subchart +# Configuration for the custom Kafka subchart. +# Kafka is the messaging backbone for Portal and always deployed. +# all external Kafka traffic flows through the APIM Kafka Proxy +# assertion. kafka: - # Enable/disable Kafka deployment - # Automatically enabled when analytics or intelligence is enabled - # Set to false to explicitly disable Kafka even when analytics/intelligence are enabled enabled: true # Global configuration passed to Kafka subchart From c95019bb89cec71a0aa4c715ad622ed10251b705 Mon Sep 17 00:00:00 2001 From: Kiran Saladi Date: Tue, 26 May 2026 21:56:34 +0530 Subject: [PATCH 6/6] Chart changes to have kafka for portal and intelligence --- charts/kafka/values.yaml | 2 +- charts/portal/charts/druid-1.0.20.tgz | Bin 9456 -> 9456 bytes charts/portal/charts/kafka-1.0.2.tgz | Bin 29170 -> 29345 bytes charts/portal/charts/seaweedfs-1.0.4.tgz | Bin 10512 -> 10690 bytes 4 files changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/kafka/values.yaml b/charts/kafka/values.yaml index 7050e80a..9876b02b 100644 --- a/charts/kafka/values.yaml +++ b/charts/kafka/values.yaml @@ -155,4 +155,4 @@ serviceAccount: kubectlImage: repository: "tls-automator" tag: "latest" - pullPolicy: IfNotPresent \ No newline at end of file + pullPolicy: IfNotPresent diff --git a/charts/portal/charts/druid-1.0.20.tgz b/charts/portal/charts/druid-1.0.20.tgz index 2c6e86f0c00eec7ed1c2cefc66ef89886dd47ba6..1c9cfb39b9385e49f342cf515c85c1d0c1039e82 100644 GIT binary patch literal 9456 zcmVDc zVQyr3R8em|NM&qo0PMYMbK5wwKRmBpMimC!%haol%uvcx-Lrlf?FsoSEIT zwdI0INaC0xH~{EG$>jO${{p}Z2;L=GlC#1KUr3TJ^MFAsEyqLcbK9~UG>0yn zrb^9i*T#e3SMY3RQ-+CcLPyJNyHiBvbQ%5Gq=-C)tQ{VBd>*078>vUYbg*xQBR!TJ z+M7a#KEXxMpvZ*}4ziRSAndQ9YkAmq*#I;efvAX&ze9`ok!(Qa(bS~nUpV63ZXuW1 z&;fr??1G8yAg-)~Z}U%yQS^uwvdGlrAO0UI)`yLd%o~iS4w97x9>ISglQsEl!Cwde zUs+)Z$XmONJA%Zj?Lv%D&q=_AVM0DS!x@;^s&F`IPzCm#aga_qTXnCS?(*g|9EAXDfPp7994$;5VTwh*#& zco2)I&=`RZRdiKcuWsjcC#T}eR<*Pa72BgiRg=myrydR;B~Qp1fZxwY*HTql=yOnj zYe7huoc>;SWH0lfVaZ1){eG|iTs?pMrnKKhmI_S~Ogq)v;2EmZDX&hqVs+FBGdYNr zH`z6l1N(J*?E|~0>itf)T!w>86{@ey#iEk-6LJxyw*)_gob${!bS{wt7h_~%*P;W^ z=?3GM*>i+_7Qc{>C(w3$g6?JnQ9iQ`Krd(qL~Lxu+53TP_(JO|`GZDjr(LShfD9=kw)XdtOTp!cysC9iv~dnU26a zIC+HHe(@)d;3wNc7I;|jIwO|>VU7WxX2=bSh<#(QQKZO&e3|Ghem*>S)YO^KUb3WABN_h0~w;N z>zljb*kGU88oyCy|81Z4HT%zTyS=ynY$V0jA4m|eUCN;2Nc#=fSb)tCxH!+T)@+;k zeOfu`@_#==j)w>}n71}laEbh%bWe2u&+%!ey_f$@q_1CFe*;hUd;ln7V*k$;9y*)z zW-(`I0RGnc_U-V1XFEI?e)gbi37xD0u3>`44$_En#xOI zn=z!M&et|ovW2SFONor#6tiw3JB@yuq@ z{Pci;PeUU*XFudOuZ4LYt(EOb)TrUxs9~c_xjCvey+ajRKZ+_0&Z_sX%4^gdl8iBu zZ6!;`{3aeR!K6oQmrX$9zi9KnXhWkXKV5Is<^Q9NN2Qy&3J;gMlM+Iq(5F z1>G%jHS%kvHoW{haO#4;#L5<$2sHk?0UGy>+Cmmq3yhIm9q6_~DI=JOmw|_P%%J&r zcmVP$l(a}+=Z<9Q3(Ck2zRA7K*RRp=YXpBZDwd80OU8GTMGR>zmmGrRSAViy)A6kg z$uxe>Bj=`A$fBICPa&Bq12pc<%jb`isIH1UHl;i^Q&e3m*(AaLIdUm_QaVE_bfwJr41F1L@E_5cVP&7G zH^W{OPT2VF^?~Y+E|}c{1%HW6?C~w*v|yMSd~ad! zJehM>12jTh4@2h6!X`CBN4|jg__T%GmI<54Wr%phrl?8rgnfbpwS;f^v?=^=!VNJ; zE^G0Bt#IEOIJH~-X4{xC-s#!(iuo2^$o=YS=T=wS-ZgzcN#FlVOKI?{DQ&x};=;=8 ze<$6Jw*Nov_PhK2|0Yrb|JgY_c(h$>04_woe}LZM!5lHj=K^7smyW;0;1DhzzF!Hw z6wvd+Eb@Watql0Q9S$>XA7ex_Vz5INnUmTh%rQg6W&%v1W(iR*JXgf*csTm)>SB0* zeg0vna`b;UH}8hS+u`W`?ala35(*S(H)rtSxXkU%D6b5kUcg`APam_e+{MlH z-TBq^aCHCv=FR>4;cvtDiNcaABlMAhjjOg|`S+}cHGo1(<^S|llmCgr7gUQ2%D)VeVJ6iYC8HLpQ5zA^|#kuQr<2 zY+t`dg;(zXV)+j&3@hq@GWqZKbo>A5@qYf_Ov;}BgZZ>z_TRZ_Khhhf<5Pyn)$IUW zV;6-m3(04A!ld!K{kj|A7bBQbH$cchGDRT{EqJPls8(5uC~v{@5FP<892>dp>UP_` zTb@efpFxHuzC+O}r^CwpfBlZ;|L?YYCwu?jMv^N3(DSG=vyX$$AE##YomK>)Rf~v& z96vsy#VhdR6Ldh@&Im0_K(e4Cr(31GCQGueGiVfKD`P`J#fL0%m>Bf((rrfecMU^tL=_H^Po%yVH*%ht#^(hsY8aV&rZi zfsTX3$&zq5gq2W-7*9^JLK#CVTC=5AUf4Q!UCacmOEpr%_~}3tX850Q?v#}O3%Isx zA(w&I3a#w*;sg^DjgSPmB`nCyo+HZO+!KxM;!oOUF>7q)J+UXi#jJ9*>#?df(z@A? zhA@cFI4G_-L0j{oWqAjU(#nNkMYUeh1fAdWQL(tsJKZYXq0v$ClS*a{R+-C2qWFre zFzg_b8WPIWB1Y80EArt99I;Pqwz$A9L!X&ageKpV2&Yb`h%<+x3}4pD%;eLl+)RO4 z9uZZ{nyKkEOcHl2ric_1$%B#L+czbXC>JzdJg+D;Zxs-8BRvnu@-fm`r;**;QRVWR zxn{rTQp{=ak`b^*2FoaxF_;(?K%9Dc`a8k%f%>c&K&?MOZ-gd;q-QeMHXEkQlLFD_ zJ}XK&OeUVsAzujoi_Ttw7WP=nvfb{_vQ?lj|QFnJ6XE$2tt^0jv|C9 z=j1`uFWDy$!jyGVW3$zbj(}{WzVRM6PR&@u+-jR#nX)A}I(MKA%J@7&fsn?9YYqBr zpig|JTge)!Ecqs`bhNq8P>?F2M!rTSC^&>BmEpPb2`*?fVHB}>P?!4mN}j-y{eSPYui5{*o!;L5znQd*|1Ui*vW)=R0l{7I1S;A=;V|Fh z_uO8>Y}KcPNHFAKc=?TS@BZWHy$r82Xtm_|{P0lpxOe~S$BTEvfEzlFhkqEM1~N;M~4JeAh+95JyG`nq!8tas|;%WVUcV$>ZnIb{qq{g*=NqVOw-A z3$t0l$<#YMLVPKFc6--(TUcTaKabILj$B61y8XKPIT|OyJE_pl%GI$3IW417Pm6}@ z+|>MEb^W&r5~R%kdwQ&||Jo-f`}ltwNy#}bp=+KQ1fr?V^wP2r4x)2f2NP1EY#}ft zRlX)Vq*D3iAR>uIt%4J&D*rOV(6j9S+WnK%{_nWGm;a5VJp6|Y=T)-#i=%$+-sVp_ zC2u_VCtpWIl~7l?qiAJWx{nAW=~dlJMEU;$4=2aSHMJ$`w!1w_)#ZPABv_gJpR`l* zzmNa9p;RCLrH6gmImxe&1nVS_V0kk&Cb1P-!-#b<=IE@p*gc?FW%*wL305rsoo=_+ z)#Sf>yqEvYq!sXAJFf`9&LP1PhLl zmu{!s-N%2}NLmK}of;U2ndbjE+u=5>|2gRIBv*Ts_s*fb%h0=V2d)a<)dy$}cDHwU zAm1N>w|959Kwi)S*o-gisZO4c7sfop_2+jo$62o(Q@ACVFDAvUlJd0(2mRnrCWs`6 zc~MS7zBQ=*1^v2Uh@N%Y$FKXRC*d7gsRGK!`|-B}RBL$KV3Kq3>H?ZGWH?w}heFT( z^|it0|Hh!vdV(Za=LLCb<5go0KacG%s4?jD+EM!U?~FDHa-J#qlvep)ACUzx`PbF5 zN>___hEI`TF@+9J<@B)Ua}|@FUFH#ZWD`y%$aXVc^X9&C!v>F8{;u%A)#m@z;=q;S ze|stXZ?AK_pZ_0oXb8pECPz1N^7!DFyqNe3c3PQ<&dP z5-E2zIJO;DfZmyP*T#j+;kFRSRXP&S3uvVgxl5^G>- zIk;aAfi>{6GPJIk){DXVC9ysFcVc0j&^HA8-T-vkopyeM7or8z8WyY!7K~}u!U`(A zR}(6zS_;>P!K*@b4;0)5px}3a6x>4v_fWx#P{E}kahX2eFzo=MEpKO{GG=6yOtrF( z4XZ>rStvzQ1C&)FnJAN@n8u)0BB~cl(Vals@d>I+S1+c~-97>^$6u`SwZ_qrq`Qv6 zk)*huv5};?wxN+ETQV}Lf+c&^R&dK0Q?Iy zMhIHJ6PuwM*F+7V==tn%7Jwlx97w4uo9grHys@QbN^Tjm#64ChNj3AQk7!Dv1*8)S z(?f*Xlp)td5r`wP_g%wXUdtri|6{vTMEPKe|62JL&=UV|uhVVo_>be$ef-~zq-F4* z@yL+iu0j;3o>{)y4J>FDZ|~0k{_gz#*U`?{7wzY3OM|SmLNq3VWK0Rig(*y%}9zU7z3GWMEwjKB$(f_s#VpUNbZ_^Vlc69XraS z1e964$b<*Q70N&dRYfRe4VLA=K-t4C4GD_bUK$aUOQI4WC@#GfJWz%D*Jk)-qk$C3 ztO*9nD7h@qB__2j6ex$n>I^Lh1j;VIIyWn$ZVE}g7!*)-3{Re71S=zXBFeRZJaPNi z#PKBdx$8p6R3N%X@$3SM=jDTV_86W$hG!q{OKG1<;l9ELn*Lf2<5b|si0PEY{z?&f zrT}2j^IlR^taU(uR*eO>W?>Ke;>*;>}-$<$-|K$bweWN9Wx(jLsfnL;2j~ePleV#IjsO|!yntZYX!(_TM zg)+JbW78aNJ7SD3|8v{295fg48A$TKrE$Pz_}`OM{I_oV^kgspn@G#xf3rqKwiZa_ z+=>dwLHS=&PczyetO9Q?LpU%XeY~{E zS$qtRze={u@_~U>D#gKp0d~PH!fs}RPcV552m8WUGJzwT4e{UqM5dZf@C<3r^M}Lwt{x$*uXJ{%V7I5lq z=6RoFEp{^^aK`9Mp8(PpV9TBYDkM|sBv6vVR!##|Y>TBBf7z#kG@-3|GAO6?vd3SN zQA#fYuHBnqt(8#Upa-fVc( z!ga!Ohj{X6)S6QPe$>I{vBR&OMacgI@e^eT9QovS904;@wd61z3YuLz=ehcr$pLW_dA^k!~`(k@lvLp-*sO zAeU=q-SQXPpFpGaV7o0kYrIl3G@2&Tp0s>QL|zM;9OhKt6}AC5nqW{kwv-Ia_YO_yldYwt|HfsxUR_b}KM7X}Z4@t~`SOKqkAgR_iKE zw_Hz5GwPY(N{+~nqxU&AP1(>_f0}C-U3Ro+O04PAq<y&LE4lV;?%uCzDwb(=LggIMxCC0>x-EO<$Q!8@|7J1&*SBqzH z46Z&%-`w21c|W{Azf~@u7!5Cn*LPRv@5h-s8r)icNJtKH%76Fm8?NZO%+MOMocA(W_9c2Pb$?^o zl1VBDZDhq7qpDKb=qb+SQ<43D)wq9QD)axHoVIoUU$=L>xBqV<>E}O%q43$yeFpXx zRSZkcd4_z|JKtGQs^nZ}RI>JY&cc$*p5qh^_8cmuNd8xi`xmAX`R{f*Y5#w}vzPx( zq}uWSzJ4v7`{ck*I|*jUnHzN0nqmAn03Jb2+ocRTPKk=xjc9>izou=GyxuVaGx&mT z1IXK(DFvgrfZtMKdNQdI=vwEFH-p#y9CX_0@_~lP<2eA2>O99={^I>)HJ&wU37(IO zc&=k#P|W}tOe^K4Cf~mO55=zj>1Oe_b1VcktpdqCb_T0iMKj_>)?!%Sg{QZ(g^a|- zD={Mf-nKT|r@E;A zZ+12J+U?#R|F@Bpix#Sb0W6Ocv4biD z{3n0_>jLhyND#r(dMLl0i2Sfe0PYch1$Ema0Cxca7>&wRBQoTWc`=a~lAtR6JF$>W z=o^B4ZvZ;&PCLJuGtiEFzJQiR5T$&|@rdm*&_4TuZ;hl!2cL=99mG?XsUiC!r;qpb88uh{cGO6a?+5s#Mh$sNz&A?0GXiEwL?yq1e~gzXPpeG5`?0<%+`-Uv1)*fXAO^$ zh5XR*Y}L?g`Pi2d#7iAglR(^*0BSODNNu(0b74r?4U`W-ssljE zZou7brW|B*5^D2=ou=tPd#=(=1%y!M&mDmftseM5HqQmG`(0>ji|!*zZE8Vrt0gu` z07#e72W)Zm@GsC9A!z+hY=&-JGkH`Y=Kzj0@?1EO(!^A$JkLLaqhw02A9Q6C>oIi% z5cH>yXbMCgkWLsKJVdBX8FEb&McWS-KYK^i5=poJ2D|Adl-udlgJlfCW&WSjb}Ifq z|FyUOZXg9J0lHQsV@ZQbhy?pdH7A9_pEAAtoH7-soJv9N9B@e;>y8`lRnvB0xrLS4 z(njYD6SJ$?jW+GSQYNitZevV*$59zwgTB+{Kf1Uo{%hqpFlF-J>$Ow&zqa@BUpA7K ziT|RHjBFu`a&1MRw%x8TB~_9CXTSeF!~fga-~Y3bl*E54*A_lA20Wo1bhT251yAx= z2M=CavJexlm9B{kuTXk9Y`D^pwJv@SQWg1s?)#rlyC=H*pLX{6?~SDT_^*WI?%K3p z1qYUbVF*N*g2PB&-WpqCm4*3_kBQT*5mZdjG7@HA)w8Og#WIKx%&>_ag;4k?1-Z>e znMI6O@Bd2jzakE-ME*}tQ}=&$PP+T`|3(rYYA&{rbhu^;LE0s|Djsa7RS4KQTv*%{ z>&1pCP_IOjXTrU5#>3K!m3la_mP4y$mtUQmm62kFr1r?Q3@36*or9HuQxVBp z@%rMft9kQKTyTxI`BWhK{oOc}g~HL$Hr>waPH~3UuU{8t=xG`9W0U>g&CR>v@OC)5 z9}h>r4M#b#tddWab)U`Gd%nwTM_rC*0i7h&(D?clK+EC5vv{+{7k_rDWL~VI zH2=3Zqq}V8L4&twwc)*7xS6c-wMRxljJ=d1$ws7r3{*?=Ep z97J(ST

A_XP-PIe<(-NA5u~36P9B(W(1?Y5y^1$eG(y7ZdahHik0$Z|eT{PPgCL z+kZBaj=(Ks3=x+ChJhRc;L{Ab;K8>Y%XX*0gXSZgB5E8S9D%!;O#$^iX&I$6?)MhRDOIG6WzaQ_42`+L9S86Uo`44g=7>y@1#FjL0e7Z@R%>drng3vzc;31==Ula*XV06$ z+)Fu~qa*N=aR zDe;AC%uqn!Epp}ol~0fY=@_@b!vegA3q($%HZ*oiy312={hr-2?dFqQKCC0;Um}&s zfBRUE|IzDr+k5%nL=xcXz+)O8_>XR{duhXYpY~~=_GzCgrvDcJ0RR6Ra@t$~W&r@R CsKGP< literal 9456 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 Cu&KcS diff --git a/charts/portal/charts/kafka-1.0.2.tgz b/charts/portal/charts/kafka-1.0.2.tgz index aadb0a09fa5cd9c1f722d9f9db979c1ad5419ab6..d4b18c5af739b3b2e91e1bfaf4f638e331e19a31 100644 GIT binary patch literal 29345 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 N|Nk*Squ&5V0RW$%5%T~5 diff --git a/charts/portal/charts/seaweedfs-1.0.4.tgz b/charts/portal/charts/seaweedfs-1.0.4.tgz index a00c492db6c4831f006471da8e175259a10934d4..b135bc5b240ec097322373a426cb97ce79eadc35 100644 GIT binary patch literal 10690 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 literal 10512 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 S-~S5$0RR8FpJe&~<^TZgy