From 28ec80e60cbd105e4393e32ef2708a3113ff427b Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Wed, 27 Aug 2025 09:42:48 -0700 Subject: [PATCH 01/13] remove unused code and add comments --- pkg/k8sutil/haProxyHelper.go | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index 1212928..cfb05ff 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -32,6 +32,7 @@ func generateFrontendConfig(cr *marklogicv1.MarklogicCluster) string { pathBasedRouting := cr.Spec.HAProxy.PathBasedRouting appServers := cr.Spec.HAProxy.AppServers if *pathBasedRouting { + // front end configuration for path based routing frontEndDef = ` frontend marklogic-pathbased-frontend mode http @@ -53,6 +54,7 @@ frontend marklogic-pathbased-frontend result += getFrontendForPathbased(data) } } else { + // front end configuration for non-path based routing frontEndDef = ` frontend marklogic-{{ .PortNumber}} mode http @@ -247,20 +249,3 @@ func parseTemplateToString(templateStr string, data interface{}) string { type Servers []marklogicv1.AppServers -func getPathList(servers Servers) []string { - var paths []string - for _, server := range servers { - paths = append(paths, server.Path) - } - return paths -} - -// returns a array of replica numbers from 0 to replicas-1 -// used for looping over replicas in haproxy config -func generateReplicaArray(replicas int) []int { - Replicas := []int{} - for i := 0; i < replicas; i++ { - Replicas = append(Replicas, i) - } - return Replicas -} From 519579a682c6c7869c91a88a661a341331d26e08 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Thu, 25 Sep 2025 01:32:57 -0700 Subject: [PATCH 02/13] the non pathbased works for app server --- pkg/k8sutil/haProxy.go | 6 +- pkg/k8sutil/haProxyHelper.go | 154 ++++++++++++++++++++++++++--------- 2 files changed, 119 insertions(+), 41 deletions(-) diff --git a/pkg/k8sutil/haProxy.go b/pkg/k8sutil/haProxy.go index 19e3cb2..5825af0 100644 --- a/pkg/k8sutil/haProxy.go +++ b/pkg/k8sutil/haProxy.go @@ -196,8 +196,10 @@ resolvers dns result += parseTemplateToString(baseConfig, data) + "\n" haProxyData["haproxy.cfg"] += result + "\n" - haProxyData["haproxy.cfg"] += generateFrontendConfig(cr) + "\n" - haProxyData["haproxy.cfg"] += generateBackendConfig(cr) + haproxyConfig := generateHAProxyConfig(cr) + + haProxyData["haproxy.cfg"] += generateFrontendConfig(cr, haproxyConfig) + "\n" + haProxyData["haproxy.cfg"] += generateBackendConfig(cr, haproxyConfig) + "\n" if cr.Spec.HAProxy.Stats.Enabled { haProxyData["haproxy.cfg"] += generateStatsConfig(cr) diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index cfb05ff..a7fd7f3 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -2,12 +2,15 @@ package k8sutil import ( "bytes" + "fmt" "text/template" marklogicv1 "github.com/marklogic/marklogic-operator-kubernetes/api/v1" ) -type HAProxyTemplateData struct { +type HAProxyTemplate struct { + FrontendName string + BackendName string TargetPortNumber int PortNumber int PortName string @@ -21,13 +24,91 @@ type HAProxyTemplateData struct { sslEnabledServer bool } +type HAProxyConfig struct { + FrontEndConfigMap map[string]FrontEndConfig + BackendConfigMap map[string][]BackendConfig +} + +type FrontEndConfig struct { + FrontendName string + PathBasedRouting bool + Port int + TargetPort int + Path string + BackendName string +} + +type BackendConfig struct { + BackendName string + GroupName string + Port int + TargetPort int + Path string + Replicas int +} + +func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { + config := &HAProxyConfig{} + frontendMap := make(map[string]FrontEndConfig) + backendMap := make(map[string][]BackendConfig) + defaultAppServer := cr.Spec.HAProxy.AppServers + groups := cr.Spec.MarkLogicGroups + for _, group := range groups { + if group.HAProxy != nil && !group.HAProxy.Enabled { + continue + } + appServers := group.HAProxy.AppServers + if len(appServers) == 0 { + appServers = defaultAppServer + } + for _, appServer := range appServers { + targetPort := int(appServer.TargetPort) + if appServer.TargetPort == 0 { + targetPort = int(appServer.Port) + } + var key string + if int(appServer.Port) == targetPort { + key = fmt.Sprintf("%d", appServer.Port) + } else { + key = fmt.Sprintf("%d-%d", appServer.Port, targetPort) + } + + frontendName := "marklogic-" + key + "-frontend" + backendName := "marklogic-" + key + "-backend" + + if _, exists := frontendMap[key]; !exists { + frontend := FrontEndConfig{ + FrontendName: frontendName, + PathBasedRouting: *cr.Spec.HAProxy.PathBasedRouting, + Port: int(appServer.Port), + TargetPort: targetPort, + BackendName: backendName, + } + frontendMap[key] = frontend + } + backend := BackendConfig{ + BackendName: backendName, + GroupName: group.Name, + Port: int(appServer.Port), + TargetPort: targetPort, + Path: appServer.Path, + Replicas: int(*group.Replicas), + } + backendMap[key] = append(backendMap[key], backend) + } + } + config.FrontEndConfigMap = frontendMap + config.BackendConfigMap = backendMap + return config +} + // generates frontend config for HAProxy depending on pathBasedRouting flag // if pathBasedRouting is disabled, it will generate a frontend for each appServer // otherwise, it will generate a single frontend with path based routing -func generateFrontendConfig(cr *marklogicv1.MarklogicCluster) string { - +func generateFrontendConfig(cr *marklogicv1.MarklogicCluster, config *HAProxyConfig) string { + frontEndConfigs := config.FrontEndConfigMap var frontEndDef string - var data *HAProxyTemplateData + var data *HAProxyTemplate var result string pathBasedRouting := cr.Spec.HAProxy.PathBasedRouting appServers := cr.Spec.HAProxy.AppServers @@ -40,13 +121,13 @@ frontend marklogic-pathbased-frontend bind :{{ .PortNumber}} {{ .SslCert }} http-request set-header Host marklogic:{{ .PortNumber}} http-request set-header REFERER http://marklogic:{{ .PortNumber}}` - data = &HAProxyTemplateData{ + data = &HAProxyTemplate{ PortNumber: int(cr.Spec.HAProxy.FrontendPort), SslCert: getSSLConfig(cr.Spec.HAProxy.Tls), } result = parseTemplateToString(frontEndDef, data) for _, appServer := range appServers { - data = &HAProxyTemplateData{ + data = &HAProxyTemplate{ PortNumber: int(appServer.Port), TargetPortNumber: int(appServer.TargetPort), Path: appServer.Path, @@ -56,16 +137,18 @@ frontend marklogic-pathbased-frontend } else { // front end configuration for non-path based routing frontEndDef = ` -frontend marklogic-{{ .PortNumber}} +frontend {{ .FrontendName }} mode http bind :{{ .PortNumber }} {{ .SslCert }} log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r" - default_backend marklogic-{{ .PortNumber}}-backend` - - for _, appServer := range appServers { - data = &HAProxyTemplateData{ - PortNumber: int(appServer.Port), - SslCert: getSSLConfig(cr.Spec.HAProxy.Tls), + default_backend {{ .BackendName }}` + for _, frontend := range frontEndConfigs { + data = &HAProxyTemplate{ + FrontendName: frontend.FrontendName, + BackendName: frontend.BackendName, + PortNumber: int(frontend.Port), + TargetPortNumber: int(frontend.TargetPort), + SslCert: getSSLConfig(cr.Spec.HAProxy.Tls), } result += parseTemplateToString(frontEndDef, data) + "\n" } @@ -74,13 +157,13 @@ frontend marklogic-{{ .PortNumber}} } // generates backend config for HAProxy depending on pathBasedRouting flag and appServers -func generateBackendConfig(cr *marklogicv1.MarklogicCluster) string { - +func generateBackendConfig(cr *marklogicv1.MarklogicCluster, config *HAProxyConfig) string { + backendConfigs := config.BackendConfigMap pathBasedRouting := cr.Spec.HAProxy.PathBasedRouting var result string backendTemplate := ` -backend marklogic-{{ .PortNumber}}-backend +backend {{ .BackendName }} mode http balance leastconn option forwardfor @@ -96,27 +179,21 @@ backend marklogic-{{ .PortNumber}}-backend backendTemplate += ` http-request replace-path {{.Path}}(/)?(.*) /\2` } - groups := cr.Spec.MarkLogicGroups - - appServers := cr.Spec.HAProxy.AppServers - - for _, appServer := range appServers { - data := &HAProxyTemplateData{ - PortNumber: int(appServer.Port), - Path: appServer.Path, + for _, backends := range backendConfigs { + data := &HAProxyTemplate{ + BackendName: backends[0].BackendName, + PortNumber: backends[0].Port, + Path: backends[0].Path, } result += parseTemplateToString(backendTemplate, data) - for _, group := range groups { - name := group.Name - groupReplicas := int(*group.Replicas) - if group.HAProxy != nil && !group.HAProxy.Enabled { - continue - } + for _, backend := range backends { + name := backend.GroupName + groupReplicas := backend.Replicas for i := 0; i < groupReplicas; i++ { - data := &HAProxyTemplateData{ - PortNumber: int(appServer.Port), + data := &HAProxyTemplate{ + PortNumber: backend.Port, PodName: name, - Path: appServer.Path, + Path: backend.Path, Index: i, ServiceName: name, NSName: cr.ObjectMeta.Namespace, @@ -132,7 +209,7 @@ backend marklogic-{{ .PortNumber}}-backend return result } -func getBackendServerConfigs(data *HAProxyTemplateData) string { +func getBackendServerConfigs(data *HAProxyTemplate) string { backend := ` server {{.PodName}}-{{.PortNumber}}-{{.Index}} {{.PodName}}-{{.Index}}.{{.ServiceName}}.{{.NSName}}.svc.{{.ClusterName}}:{{.PortNumber}} resolvers dns init-addr none cookie {{.PodName}}-{{.PortNumber}}-{{.Index}}` if data.sslEnabledServer { @@ -142,7 +219,7 @@ func getBackendServerConfigs(data *HAProxyTemplateData) string { return parseTemplateToString(backend, data) } -func getFrontendForPathbased(data *HAProxyTemplateData) string { +func getFrontendForPathbased(data *HAProxyTemplate) string { frontend := ` use_backend marklogic-{{.PortNumber}}-backend if { path {{.Path}} } || { path_beg {{.Path}}/ }` if data.PortNumber == 8000 || data.TargetPortNumber == 8000 { @@ -158,7 +235,7 @@ func getFrontendForPathbased(data *HAProxyTemplateData) string { return parseTemplateToString(frontend, data) } -func getBackendForTCP(data *HAProxyTemplateData) string { +func getBackendForTCP(data *HAProxyTemplate) string { backend := ` server ml-{{.PodName}}-{{.PortNumber}}-{{.Index}} {{.PodName}}-{{.Index}}.{{.ServiceName}}.{{.NSName}}.svc.{{.ClusterName}}:{{.PortNumber}} check resolvers dns init-addr none` return parseTemplateToString(backend, data) @@ -200,7 +277,7 @@ listen marklogic-TCP-{{.PortNumber}} bind :{{ .PortNumber }} {{ .SslCert }} mode tcp balance leastconn` - data := &HAProxyTemplateData{ + data := &HAProxyTemplate{ PortNumber: int(tcpPort.Port), SslCert: getSSLConfig(cr.Spec.HAProxy.Tls), } @@ -212,7 +289,7 @@ listen marklogic-TCP-{{.PortNumber}} continue } for i := 0; i < groupReplicas; i++ { - data := &HAProxyTemplateData{ + data := &HAProxyTemplate{ PortNumber: int(tcpPort.Port), PodName: name, Index: i, @@ -248,4 +325,3 @@ func parseTemplateToString(templateStr string, data interface{}) string { } type Servers []marklogicv1.AppServers - From e3a0292401e2a93240a5c0fe3c7344092fd002b1 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Fri, 26 Sep 2025 07:08:21 -0700 Subject: [PATCH 03/13] support for pathbased routing --- pkg/k8sutil/haProxyHelper.go | 89 ++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index a7fd7f3..49a9d47 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -3,6 +3,7 @@ package k8sutil import ( "bytes" "fmt" + "strings" "text/template" marklogicv1 "github.com/marklogic/marklogic-operator-kubernetes/api/v1" @@ -25,6 +26,7 @@ type HAProxyTemplate struct { } type HAProxyConfig struct { + IsPathBased bool FrontEndConfigMap map[string]FrontEndConfig BackendConfigMap map[string][]BackendConfig } @@ -39,6 +41,7 @@ type FrontEndConfig struct { } type BackendConfig struct { + IsPathBased bool BackendName string GroupName string Port int @@ -53,11 +56,23 @@ func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { backendMap := make(map[string][]BackendConfig) defaultAppServer := cr.Spec.HAProxy.AppServers groups := cr.Spec.MarkLogicGroups + config.IsPathBased = *cr.Spec.HAProxy.PathBasedRouting for _, group := range groups { if group.HAProxy != nil && !group.HAProxy.Enabled { continue + } else { + // setting is path-based + if !config.IsPathBased && *group.HAProxy.PathBasedRouting == true { + config.IsPathBased = true + } } appServers := group.HAProxy.AppServers + var groupPathBased bool + if group.HAProxy.PathBasedRouting != nil { + groupPathBased = *group.HAProxy.PathBasedRouting + } else { + groupPathBased = *cr.Spec.HAProxy.PathBasedRouting + } if len(appServers) == 0 { appServers = defaultAppServer } @@ -67,24 +82,31 @@ func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { targetPort = int(appServer.Port) } var key string - if int(appServer.Port) == targetPort { - key = fmt.Sprintf("%d", appServer.Port) + if groupPathBased { + if int(appServer.Port) == targetPort { + key = fmt.Sprintf("%d", appServer.Port) + } else { + key = fmt.Sprintf("%d-%d", appServer.Port, targetPort) + } } else { - key = fmt.Sprintf("%d-%d", appServer.Port, targetPort) + pathWithoutSlashes := strings.ReplaceAll(appServer.Path, "/", "") + key = fmt.Sprintf("%d-%s-path", appServer.Port, pathWithoutSlashes) } - frontendName := "marklogic-" + key + "-frontend" backendName := "marklogic-" + key + "-backend" - - if _, exists := frontendMap[key]; !exists { - frontend := FrontEndConfig{ - FrontendName: frontendName, - PathBasedRouting: *cr.Spec.HAProxy.PathBasedRouting, - Port: int(appServer.Port), - TargetPort: targetPort, - BackendName: backendName, + // only add frontend when pathBasedRouting is set to false for the group + if !groupPathBased { + frontendName := "marklogic-" + key + "-frontend" + if _, exists := frontendMap[key]; !exists { + frontend := FrontEndConfig{ + FrontendName: frontendName, + PathBasedRouting: *cr.Spec.HAProxy.PathBasedRouting, + Port: int(appServer.Port), + TargetPort: targetPort, + BackendName: backendName, + } + frontendMap[key] = frontend } - frontendMap[key] = frontend } backend := BackendConfig{ BackendName: backendName, @@ -93,6 +115,7 @@ func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { TargetPort: targetPort, Path: appServer.Path, Replicas: int(*group.Replicas), + IsPathBased: groupPathBased, } backendMap[key] = append(backendMap[key], backend) } @@ -110,9 +133,7 @@ func generateFrontendConfig(cr *marklogicv1.MarklogicCluster, config *HAProxyCon var frontEndDef string var data *HAProxyTemplate var result string - pathBasedRouting := cr.Spec.HAProxy.PathBasedRouting - appServers := cr.Spec.HAProxy.AppServers - if *pathBasedRouting { + if config.IsPathBased { // front end configuration for path based routing frontEndDef = ` frontend marklogic-pathbased-frontend @@ -126,32 +147,32 @@ frontend marklogic-pathbased-frontend SslCert: getSSLConfig(cr.Spec.HAProxy.Tls), } result = parseTemplateToString(frontEndDef, data) - for _, appServer := range appServers { + for _, backends := range config.BackendConfigMap { data = &HAProxyTemplate{ - PortNumber: int(appServer.Port), - TargetPortNumber: int(appServer.TargetPort), - Path: appServer.Path, + PortNumber: int(backends[0].Port), + TargetPortNumber: int(backends[0].TargetPort), + Path: backends[0].Path, + BackendName: backends[0].BackendName, } result += getFrontendForPathbased(data) } - } else { - // front end configuration for non-path based routing - frontEndDef = ` + } + // front end configuration for non-path based routing + frontEndDef = ` frontend {{ .FrontendName }} mode http bind :{{ .PortNumber }} {{ .SslCert }} log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r" default_backend {{ .BackendName }}` - for _, frontend := range frontEndConfigs { - data = &HAProxyTemplate{ - FrontendName: frontend.FrontendName, - BackendName: frontend.BackendName, - PortNumber: int(frontend.Port), - TargetPortNumber: int(frontend.TargetPort), - SslCert: getSSLConfig(cr.Spec.HAProxy.Tls), - } - result += parseTemplateToString(frontEndDef, data) + "\n" + for _, frontend := range frontEndConfigs { + data = &HAProxyTemplate{ + FrontendName: frontend.FrontendName, + BackendName: frontend.BackendName, + PortNumber: int(frontend.Port), + TargetPortNumber: int(frontend.TargetPort), + SslCert: getSSLConfig(cr.Spec.HAProxy.Tls), } + result += parseTemplateToString(frontEndDef, data) + "\n" } return result } @@ -191,7 +212,7 @@ backend {{ .BackendName }} groupReplicas := backend.Replicas for i := 0; i < groupReplicas; i++ { data := &HAProxyTemplate{ - PortNumber: backend.Port, + PortNumber: backend.TargetPort, PodName: name, Path: backend.Path, Index: i, @@ -221,7 +242,7 @@ func getBackendServerConfigs(data *HAProxyTemplate) string { func getFrontendForPathbased(data *HAProxyTemplate) string { frontend := ` - use_backend marklogic-{{.PortNumber}}-backend if { path {{.Path}} } || { path_beg {{.Path}}/ }` + use_backend {{.BackendName}} if { path {{.Path}} } || { path_beg {{.Path}}/ }` if data.PortNumber == 8000 || data.TargetPortNumber == 8000 { frontend += ` http-request set-header X-ML-QC-Path {{.Path}}` From bd08a14c565b4cea42ec1aceaea4cd6c5659f08b Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Mon, 29 Sep 2025 09:46:42 -0700 Subject: [PATCH 04/13] fix typo --- pkg/k8sutil/haProxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/k8sutil/haProxy.go b/pkg/k8sutil/haProxy.go index 5825af0..56c8c33 100644 --- a/pkg/k8sutil/haProxy.go +++ b/pkg/k8sutil/haProxy.go @@ -233,7 +233,7 @@ func (cc *ClusterContext) createHAProxyDeploymentDef(meta metav1.ObjectMeta) *ap ObjectMeta: metav1.ObjectMeta{ Labels: meta.Labels, Annotations: map[string]string{ - "comfigmap-hash": calculateHash(generateHAProxyConfigMapData(cr)), + "configmap-hash": calculateHash(generateHAProxyConfigMapData(cr)), }, }, Spec: corev1.PodSpec{ From 71a37d73670b1e8164239e8c2bdf36f90f1f318d Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Mon, 29 Sep 2025 15:25:58 -0700 Subject: [PATCH 05/13] fix bugs for pathbased routing --- pkg/k8sutil/haProxyHelper.go | 62 +++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index 49a9d47..d1e021a 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -23,6 +23,7 @@ type HAProxyTemplate struct { ClusterName string SslCert string sslEnabledServer bool + IsPathBased bool } type HAProxyConfig struct { @@ -32,17 +33,17 @@ type HAProxyConfig struct { } type FrontEndConfig struct { - FrontendName string - PathBasedRouting bool - Port int - TargetPort int - Path string - BackendName string + FrontendName string + IsPathBased bool + Port int + TargetPort int + Path string + BackendName string } type BackendConfig struct { - IsPathBased bool BackendName string + IsPathBased bool GroupName string Port int TargetPort int @@ -67,11 +68,9 @@ func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { } } appServers := group.HAProxy.AppServers - var groupPathBased bool + groupPathBased := *cr.Spec.HAProxy.PathBasedRouting if group.HAProxy.PathBasedRouting != nil { groupPathBased = *group.HAProxy.PathBasedRouting - } else { - groupPathBased = *cr.Spec.HAProxy.PathBasedRouting } if len(appServers) == 0 { appServers = defaultAppServer @@ -82,7 +81,7 @@ func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { targetPort = int(appServer.Port) } var key string - if groupPathBased { + if !groupPathBased { if int(appServer.Port) == targetPort { key = fmt.Sprintf("%d", appServer.Port) } else { @@ -90,7 +89,7 @@ func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { } } else { pathWithoutSlashes := strings.ReplaceAll(appServer.Path, "/", "") - key = fmt.Sprintf("%d-%s-path", appServer.Port, pathWithoutSlashes) + key = fmt.Sprintf("%d-%s-path", appServer.TargetPort, pathWithoutSlashes) } backendName := "marklogic-" + key + "-backend" @@ -99,11 +98,11 @@ func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { frontendName := "marklogic-" + key + "-frontend" if _, exists := frontendMap[key]; !exists { frontend := FrontEndConfig{ - FrontendName: frontendName, - PathBasedRouting: *cr.Spec.HAProxy.PathBasedRouting, - Port: int(appServer.Port), - TargetPort: targetPort, - BackendName: backendName, + FrontendName: frontendName, + IsPathBased: groupPathBased, + Port: int(appServer.Port), + TargetPort: targetPort, + BackendName: backendName, } frontendMap[key] = frontend } @@ -148,13 +147,20 @@ frontend marklogic-pathbased-frontend } result = parseTemplateToString(frontEndDef, data) for _, backends := range config.BackendConfigMap { - data = &HAProxyTemplate{ - PortNumber: int(backends[0].Port), - TargetPortNumber: int(backends[0].TargetPort), - Path: backends[0].Path, - BackendName: backends[0].BackendName, + for _, babackend := range backends { + if !babackend.IsPathBased { + continue + } + data = &HAProxyTemplate{ + PortNumber: int(babackend.Port), + TargetPortNumber: int(babackend.TargetPort), + Path: babackend.Path, + IsPathBased: babackend.IsPathBased, + BackendName: babackend.BackendName, + } + result += getFrontendForPathbased(data) + } - result += getFrontendForPathbased(data) } } // front end configuration for non-path based routing @@ -180,7 +186,6 @@ frontend {{ .FrontendName }} // generates backend config for HAProxy depending on pathBasedRouting flag and appServers func generateBackendConfig(cr *marklogicv1.MarklogicCluster, config *HAProxyConfig) string { backendConfigs := config.BackendConfigMap - pathBasedRouting := cr.Spec.HAProxy.PathBasedRouting var result string backendTemplate := ` @@ -195,11 +200,6 @@ backend {{ .BackendName }} stick match req.cook(HostId) stick match req.cook(SessionId) default-server check` - - if *pathBasedRouting { - backendTemplate += ` - http-request replace-path {{.Path}}(/)?(.*) /\2` - } for _, backends := range backendConfigs { data := &HAProxyTemplate{ BackendName: backends[0].BackendName, @@ -210,6 +210,10 @@ backend {{ .BackendName }} for _, backend := range backends { name := backend.GroupName groupReplicas := backend.Replicas + if backend.IsPathBased { + backendTemplate += ` + http-request replace-path {{.Path}}(/)?(.*) /\2` + } for i := 0; i < groupReplicas; i++ { data := &HAProxyTemplate{ PortNumber: backend.TargetPort, From 63a4f1978f6f212b9dbefb259e894b99c39b442f Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Mon, 29 Sep 2025 22:20:02 -0700 Subject: [PATCH 06/13] add TCP implementation --- api/v1/common_types.go | 9 +- api/v1/zz_generated.deepcopy.go | 6 +- ...klogic.progress.com_marklogicclusters.yaml | 6 ++ pkg/k8sutil/haProxy.go | 4 +- pkg/k8sutil/haProxyHelper.go | 87 ++++++++++++++++--- 5 files changed, 90 insertions(+), 22 deletions(-) diff --git a/api/v1/common_types.go b/api/v1/common_types.go index f0d6888..b5d916f 100644 --- a/api/v1/common_types.go +++ b/api/v1/common_types.go @@ -99,7 +99,7 @@ type HAProxy struct { PathBasedRouting *bool `json:"pathBasedRouting,omitempty"` Service *corev1.ServiceType `json:"service,omitempty"` // +kubebuilder:default:={enabled: false} - TcpPorts Tcpports `json:"tcpPorts,omitempty"` + TcpPorts *Tcpports `json:"tcpPorts,omitempty"` // +kubebuilder:default:={client: 600, connect: 600, server: 600} Timeout Timeout `json:"timeout,omitempty"` // +kubebuilder:default:={enabled: false, secretName: "", certFileName: ""} @@ -138,9 +138,10 @@ type Tcpports struct { } type TcpPort struct { - Port int32 `json:"port,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` + Port int32 `json:"port,omitempty"` + TargetPort int32 `json:"targetPort,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` } type Timeout struct { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 8ab1994..ef51663 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -130,7 +130,11 @@ func (in *HAProxy) DeepCopyInto(out *HAProxy) { *out = new(corev1.ServiceType) **out = **in } - in.TcpPorts.DeepCopyInto(&out.TcpPorts) + if in.TcpPorts != nil { + in, out := &in.TcpPorts, &out.TcpPorts + *out = new(Tcpports) + (*in).DeepCopyInto(*out) + } out.Timeout = in.Timeout if in.Tls != nil { in, out := &in.Tls, &out.Tls diff --git a/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml b/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml index 9d27b2f..543d268 100644 --- a/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml +++ b/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml @@ -4544,6 +4544,9 @@ spec: port: format: int32 type: integer + targetPort: + format: int32 + type: integer type: type: string type: object @@ -9316,6 +9319,9 @@ spec: port: format: int32 type: integer + targetPort: + format: int32 + type: integer type: type: string type: object diff --git a/pkg/k8sutil/haProxy.go b/pkg/k8sutil/haProxy.go index 56c8c33..36e130f 100644 --- a/pkg/k8sutil/haProxy.go +++ b/pkg/k8sutil/haProxy.go @@ -205,9 +205,7 @@ resolvers dns haProxyData["haproxy.cfg"] += generateStatsConfig(cr) } - if cr.Spec.HAProxy.TcpPorts.Enabled { - haProxyData["haproxy.cfg"] += generateTcpConfig(cr) + "\n" - } + haProxyData["haproxy.cfg"] += generateTcpConfig(cr, haproxyConfig) + "\n" return haProxyData } diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index d1e021a..d88833f 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -12,6 +12,7 @@ import ( type HAProxyTemplate struct { FrontendName string BackendName string + TcpName string TargetPortNumber int PortNumber int PortName string @@ -30,6 +31,7 @@ type HAProxyConfig struct { IsPathBased bool FrontEndConfigMap map[string]FrontEndConfig BackendConfigMap map[string][]BackendConfig + TCPConfigMap map[string][]TCPConfig } type FrontEndConfig struct { @@ -51,10 +53,21 @@ type BackendConfig struct { Replicas int } +type TCPConfig struct { + TcpName string + Port int + TargetPort int + PortName string + PodName string + Replicas int + GroupName string +} + func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { config := &HAProxyConfig{} frontendMap := make(map[string]FrontEndConfig) backendMap := make(map[string][]BackendConfig) + tcpMap := make(map[string][]TCPConfig) defaultAppServer := cr.Spec.HAProxy.AppServers groups := cr.Spec.MarkLogicGroups config.IsPathBased = *cr.Spec.HAProxy.PathBasedRouting @@ -67,6 +80,51 @@ func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { config.IsPathBased = true } } + // process tcp ports + if cr.Spec.HAProxy.TcpPorts != nil && group.HAProxy.TcpPorts != nil && group.HAProxy.TcpPorts.Enabled { + tcpPorts := cr.Spec.HAProxy.TcpPorts.Ports + if group.HAProxy != nil && group.HAProxy.TcpPorts != nil { + tcpPorts = group.HAProxy.TcpPorts.Ports + } + if len(tcpPorts) == 0 { + tcpPorts = []marklogicv1.TcpPort{} + } + for _, tcpPort := range tcpPorts { + targetPort := int(tcpPort.TargetPort) + if tcpPort.TargetPort == 0 { + targetPort = int(tcpPort.Port) + } + var key string + if int(tcpPort.Port) == targetPort { + key = fmt.Sprintf("%d", tcpPort.Port) + } else { + key = fmt.Sprintf("%d-%d", tcpPort.Port, targetPort) + } + if _, exists := tcpMap[key]; exists { + tcpMap[key] = append(tcpMap[key], TCPConfig{ + TcpName: key, + Port: int(tcpPort.Port), + TargetPort: targetPort, + PortName: tcpPort.Name, + PodName: group.Name, + Replicas: int(*group.Replicas), + GroupName: group.Name, + }) + } else { + tcpMap[key] = []TCPConfig{{ + TcpName: key, + Port: int(tcpPort.Port), + TargetPort: targetPort, + PortName: tcpPort.Name, + PodName: group.Name, + Replicas: int(*group.Replicas), + GroupName: group.Name, + }} + } + } + } + + // process http ports with appServers appServers := group.HAProxy.AppServers groupPathBased := *cr.Spec.HAProxy.PathBasedRouting if group.HAProxy.PathBasedRouting != nil { @@ -121,6 +179,7 @@ func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { } config.FrontEndConfigMap = frontendMap config.BackendConfigMap = backendMap + config.TCPConfigMap = tcpMap return config } @@ -293,30 +352,31 @@ frontend stats } // generates the tcp config for HAProxy -func generateTcpConfig(cr *marklogicv1.MarklogicCluster) string { +func generateTcpConfig(cr *marklogicv1.MarklogicCluster, config *HAProxyConfig) string { result := "" - - for _, tcpPort := range cr.Spec.HAProxy.TcpPorts.Ports { + tcpConfigs := config.TCPConfigMap + if len(tcpConfigs) == 0 { + return result + } + for _, tcpConfigSlice := range tcpConfigs { t := ` -listen marklogic-TCP-{{.PortNumber}} +listen marklogic-TCP-{{.TcpName }} bind :{{ .PortNumber }} {{ .SslCert }} mode tcp balance leastconn` data := &HAProxyTemplate{ - PortNumber: int(tcpPort.Port), + PortNumber: int(tcpConfigSlice[0].Port), + TcpName: tcpConfigSlice[0].TcpName, SslCert: getSSLConfig(cr.Spec.HAProxy.Tls), } result += parseTemplateToString(t, data) - for _, group := range cr.Spec.MarkLogicGroups { - name := group.Name - groupReplicas := int(*group.Replicas) - if group.HAProxy != nil && !group.HAProxy.Enabled { - continue - } + name := tcpConfigSlice[0].GroupName + groupReplicas := int(tcpConfigSlice[0].Replicas) + for _, tcpConfig := range tcpConfigSlice { for i := 0; i < groupReplicas; i++ { data := &HAProxyTemplate{ - PortNumber: int(tcpPort.Port), - PodName: name, + PortNumber: int(tcpConfig.TargetPort), + PodName: tcpConfig.PodName, Index: i, ServiceName: name, NSName: cr.ObjectMeta.Namespace, @@ -326,7 +386,6 @@ listen marklogic-TCP-{{.PortNumber}} } } } - return result } From 20618f3e7c8ff910deb5196a12b6939e4980d932 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Tue, 7 Oct 2025 22:01:49 -0700 Subject: [PATCH 07/13] fix null pointer issue --- api/v1/common_types.go | 3 +- ...klogic.progress.com_marklogicclusters.yaml | 4 --- pkg/k8sutil/haProxy.go | 9 ++--- pkg/k8sutil/haProxyHelper.go | 34 ++++++++++++------- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/api/v1/common_types.go b/api/v1/common_types.go index b5d916f..da2c0a1 100644 --- a/api/v1/common_types.go +++ b/api/v1/common_types.go @@ -98,8 +98,7 @@ type HAProxy struct { // +kubebuilder:default:=false PathBasedRouting *bool `json:"pathBasedRouting,omitempty"` Service *corev1.ServiceType `json:"service,omitempty"` - // +kubebuilder:default:={enabled: false} - TcpPorts *Tcpports `json:"tcpPorts,omitempty"` + TcpPorts *Tcpports `json:"tcpPorts,omitempty"` // +kubebuilder:default:={client: 600, connect: 600, server: 600} Timeout Timeout `json:"timeout,omitempty"` // +kubebuilder:default:={enabled: false, secretName: "", certFileName: ""} diff --git a/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml b/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml index 543d268..ae20a6f 100644 --- a/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml +++ b/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml @@ -4531,8 +4531,6 @@ spec: type: integer type: object tcpPorts: - default: - enabled: false properties: enabled: type: boolean @@ -9306,8 +9304,6 @@ spec: type: integer type: object tcpPorts: - default: - enabled: false properties: enabled: type: boolean diff --git a/pkg/k8sutil/haProxy.go b/pkg/k8sutil/haProxy.go index 36e130f..e848bc4 100644 --- a/pkg/k8sutil/haProxy.go +++ b/pkg/k8sutil/haProxy.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "sort" + "context" "github.com/cisco-open/k8s-objectmatcher/patch" marklogicv1 "github.com/marklogic/marklogic-operator-kubernetes/api/v1" "github.com/marklogic/marklogic-operator-kubernetes/pkg/result" @@ -33,7 +34,7 @@ func (cc *ClusterContext) ReconcileHAProxy() result.ReconcileResult { configmap := &corev1.ConfigMap{} haproxyService := &corev1.Service{} err := client.Get(cc.Ctx, nsName, configmap) - data := generateHAProxyConfigMapData(cc.MarklogicCluster) + data := generateHAProxyConfigMapData(cc.Ctx, cc.MarklogicCluster) configMapDef := generateHAProxyConfigMap(objectMeta, marklogicClusterAsOwner(cr), data) haproxyDeploymentDef := cc.createHAProxyDeploymentDef(objectMeta) haproxyServiceDef := cc.generateHaproxyServiceDef(objectMeta) @@ -143,7 +144,7 @@ func (cc *ClusterContext) ReconcileHAProxy() result.ReconcileResult { } // generateHAProxyData generates the HAProxy Config Data -func generateHAProxyConfigMapData(cr *marklogicv1.MarklogicCluster) map[string]string { +func generateHAProxyConfigMapData(ctx context.Context, cr *marklogicv1.MarklogicCluster) map[string]string { var result string // HAProxy Config Data haProxyData := make(map[string]string) @@ -196,7 +197,7 @@ resolvers dns result += parseTemplateToString(baseConfig, data) + "\n" haProxyData["haproxy.cfg"] += result + "\n" - haproxyConfig := generateHAProxyConfig(cr) + haproxyConfig := generateHAProxyConfig(ctx, cr) haProxyData["haproxy.cfg"] += generateFrontendConfig(cr, haproxyConfig) + "\n" haProxyData["haproxy.cfg"] += generateBackendConfig(cr, haproxyConfig) + "\n" @@ -231,7 +232,7 @@ func (cc *ClusterContext) createHAProxyDeploymentDef(meta metav1.ObjectMeta) *ap ObjectMeta: metav1.ObjectMeta{ Labels: meta.Labels, Annotations: map[string]string{ - "configmap-hash": calculateHash(generateHAProxyConfigMapData(cr)), + "configmap-hash": calculateHash(generateHAProxyConfigMapData(cc.Ctx, cr)), }, }, Spec: corev1.PodSpec{ diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index d88833f..923c176 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -2,11 +2,12 @@ package k8sutil import ( "bytes" + "context" "fmt" + marklogicv1 "github.com/marklogic/marklogic-operator-kubernetes/api/v1" + "sigs.k8s.io/controller-runtime/pkg/log" "strings" "text/template" - - marklogicv1 "github.com/marklogic/marklogic-operator-kubernetes/api/v1" ) type HAProxyTemplate struct { @@ -63,7 +64,9 @@ type TCPConfig struct { GroupName string } -func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { +func generateHAProxyConfig(ctx context.Context, cr *marklogicv1.MarklogicCluster) *HAProxyConfig { + logger := log.FromContext(ctx) + logger.Info("Generating HAProxy configuration") config := &HAProxyConfig{} frontendMap := make(map[string]FrontEndConfig) backendMap := make(map[string][]BackendConfig) @@ -80,14 +83,21 @@ func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { config.IsPathBased = true } } + + groupHAConfig := group.HAProxy + + if groupHAConfig == nil { + groupHAConfig = cr.Spec.HAProxy + } + // process tcp ports - if cr.Spec.HAProxy.TcpPorts != nil && group.HAProxy.TcpPorts != nil && group.HAProxy.TcpPorts.Enabled { - tcpPorts := cr.Spec.HAProxy.TcpPorts.Ports - if group.HAProxy != nil && group.HAProxy.TcpPorts != nil { - tcpPorts = group.HAProxy.TcpPorts.Ports + if groupHAConfig.TcpPorts != nil && groupHAConfig.TcpPorts.Enabled { + tcpPorts := []marklogicv1.TcpPort{} + if cr.Spec.HAProxy.TcpPorts != nil { + tcpPorts = cr.Spec.HAProxy.TcpPorts.Ports } - if len(tcpPorts) == 0 { - tcpPorts = []marklogicv1.TcpPort{} + if groupHAConfig.TcpPorts != nil { + tcpPorts = groupHAConfig.TcpPorts.Ports } for _, tcpPort := range tcpPorts { targetPort := int(tcpPort.TargetPort) @@ -125,10 +135,10 @@ func generateHAProxyConfig(cr *marklogicv1.MarklogicCluster) *HAProxyConfig { } // process http ports with appServers - appServers := group.HAProxy.AppServers + appServers := groupHAConfig.AppServers groupPathBased := *cr.Spec.HAProxy.PathBasedRouting - if group.HAProxy.PathBasedRouting != nil { - groupPathBased = *group.HAProxy.PathBasedRouting + if groupHAConfig.PathBasedRouting != nil { + groupPathBased = *groupHAConfig.PathBasedRouting } if len(appServers) == 0 { appServers = defaultAppServer From e3f8e55fec200584aa6ad42f138b4454c757bc76 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Tue, 7 Oct 2025 23:06:56 -0700 Subject: [PATCH 08/13] fix nil pointer issue --- pkg/k8sutil/haProxyHelper.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index 923c176..1cd58dd 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -4,10 +4,11 @@ import ( "bytes" "context" "fmt" - marklogicv1 "github.com/marklogic/marklogic-operator-kubernetes/api/v1" - "sigs.k8s.io/controller-runtime/pkg/log" "strings" "text/template" + + marklogicv1 "github.com/marklogic/marklogic-operator-kubernetes/api/v1" + "sigs.k8s.io/controller-runtime/pkg/log" ) type HAProxyTemplate struct { @@ -77,11 +78,10 @@ func generateHAProxyConfig(ctx context.Context, cr *marklogicv1.MarklogicCluster for _, group := range groups { if group.HAProxy != nil && !group.HAProxy.Enabled { continue - } else { - // setting is path-based - if !config.IsPathBased && *group.HAProxy.PathBasedRouting == true { - config.IsPathBased = true - } + } + + if !config.IsPathBased && group.HAProxy != nil && group.HAProxy.PathBasedRouting != nil && *group.HAProxy.PathBasedRouting == true { + config.IsPathBased = true } groupHAConfig := group.HAProxy From 9ec1fa2e34d7a5d529b67629a35e1084368ec607 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Tue, 7 Oct 2025 23:44:55 -0700 Subject: [PATCH 09/13] add HAProxyGroup --- api/v1/common_types.go | 8 + api/v1/marklogiccluster_types.go | 2 +- api/v1/zz_generated.deepcopy.go | 32 +- ...klogic.progress.com_marklogicclusters.yaml | 1283 +---------------- pkg/k8sutil/haProxyHelper.go | 50 +- 5 files changed, 80 insertions(+), 1295 deletions(-) diff --git a/api/v1/common_types.go b/api/v1/common_types.go index da2c0a1..c0a1831 100644 --- a/api/v1/common_types.go +++ b/api/v1/common_types.go @@ -111,6 +111,14 @@ type HAProxy struct { Ingress Ingress `json:"ingress,omitempty"` } +// HAProxyGroup represents group-level HAProxy configuration that can override cluster settings +type HAProxyGroup struct { + Enabled bool `json:"enabled,omitempty"` + AppServers []AppServers `json:"appServers,omitempty"` + PathBasedRouting *bool `json:"pathBasedRouting,omitempty"` + TcpPorts *Tcpports `json:"tcpPorts,omitempty"` +} + type AppServers struct { Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` diff --git a/api/v1/marklogiccluster_types.go b/api/v1/marklogiccluster_types.go index 5453f7b..fce5b62 100644 --- a/api/v1/marklogiccluster_types.go +++ b/api/v1/marklogiccluster_types.go @@ -108,7 +108,7 @@ type MarklogicGroups struct { PriorityClassName string `json:"priorityClassName,omitempty"` HugePages *HugePages `json:"hugePages,omitempty"` LogCollection *LogCollection `json:"logCollection,omitempty"` - HAProxy *HAProxy `json:"haproxy,omitempty"` + HAProxy *HAProxyGroup `json:"haproxy,omitempty"` // +kubebuilder:default:=false IsBootstrap bool `json:"isBootstrap,omitempty"` Tls *Tls `json:"tls,omitempty"` diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index ef51663..6903cf6 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -168,6 +168,36 @@ func (in *HAProxy) DeepCopy() *HAProxy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HAProxyGroup) DeepCopyInto(out *HAProxyGroup) { + *out = *in + if in.AppServers != nil { + in, out := &in.AppServers, &out.AppServers + *out = make([]AppServers, len(*in)) + copy(*out, *in) + } + if in.PathBasedRouting != nil { + in, out := &in.PathBasedRouting, &out.PathBasedRouting + *out = new(bool) + **out = **in + } + if in.TcpPorts != nil { + in, out := &in.TcpPorts, &out.TcpPorts + *out = new(Tcpports) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HAProxyGroup. +func (in *HAProxyGroup) DeepCopy() *HAProxyGroup { + if in == nil { + return nil + } + out := new(HAProxyGroup) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HugePages) DeepCopyInto(out *HugePages) { *out = *in @@ -817,7 +847,7 @@ func (in *MarklogicGroups) DeepCopyInto(out *MarklogicGroups) { } if in.HAProxy != nil { in, out := &in.HAProxy, &out.HAProxy - *out = new(HAProxy) + *out = new(HAProxyGroup) (*in).DeepCopyInto(*out) } if in.Tls != nil { diff --git a/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml b/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml index ae20a6f..7b2f3a8 100644 --- a/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml +++ b/config/crd/bases/marklogic.progress.com_marklogicclusters.yaml @@ -8029,940 +8029,9 @@ spec: type: string type: object haproxy: + description: HAProxyGroup represents group-level HAProxy configuration + that can override cluster settings properties: - affinity: - description: Affinity is a group of affinity scheduling - rules. - properties: - nodeAffinity: - description: Describes node affinity scheduling rules - for the pod. - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: |- - The scheduler will prefer to schedule pods to nodes that satisfy - the affinity expressions specified by this field, but it may choose - a node that violates one or more of the expressions. The node that is - most preferred is the one with the greatest sum of weights, i.e. - for each node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, etc.), - compute a sum by iterating through the elements of this field and adding - "weight" to the sum if the node matches the corresponding matchExpressions; the - node(s) with the highest sum are the most preferred. - items: - description: |- - An empty preferred scheduling term matches all objects with implicit weight 0 - (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). - properties: - preference: - description: A node selector term, associated - with the corresponding weight. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that - the selector applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that - the selector applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - type: object - x-kubernetes-map-type: atomic - weight: - description: Weight associated with matching - the corresponding nodeSelectorTerm, in the - range 1-100. - format: int32 - type: integer - required: - - preference - - weight - type: object - type: array - x-kubernetes-list-type: atomic - requiredDuringSchedulingIgnoredDuringExecution: - description: |- - If the affinity requirements specified by this field are not met at - scheduling time, the pod will not be scheduled onto the node. - If the affinity requirements specified by this field cease to be met - at some point during pod execution (e.g. due to an update), the system - may or may not try to eventually evict the pod from its node. - properties: - nodeSelectorTerms: - description: Required. A list of node selector - terms. The terms are ORed. - items: - description: |- - A null or empty node selector term matches no objects. The requirements of - them are ANDed. - The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that - the selector applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that - the selector applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - type: object - x-kubernetes-map-type: atomic - type: array - x-kubernetes-list-type: atomic - required: - - nodeSelectorTerms - type: object - x-kubernetes-map-type: atomic - type: object - podAffinity: - description: Describes pod affinity scheduling rules - (e.g. co-locate this pod in the same node, zone, etc. - as some other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: |- - The scheduler will prefer to schedule pods to nodes that satisfy - the affinity expressions specified by this field, but it may choose - a node that violates one or more of the expressions. The node that is - most preferred is the one with the greatest sum of weights, i.e. - for each node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, etc.), - compute a sum by iterating through the elements of this field and adding - "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the - node(s) with the highest sum are the most preferred. - items: - description: The weights of all of the matched - WeightedPodAffinityTerm fields are added per-node - to find the most preferred node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, - associated with the corresponding weight. - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a - list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label - key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default). - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default). - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a - list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label - key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: |- - weight associated with matching the corresponding podAffinityTerm, - in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - x-kubernetes-list-type: atomic - requiredDuringSchedulingIgnoredDuringExecution: - description: |- - If the affinity requirements specified by this field are not met at - scheduling time, the pod will not be scheduled onto the node. - If the affinity requirements specified by this field cease to be met - at some point during pod execution (e.g. due to a pod label update), the - system may or may not try to eventually evict the pod from its node. - When there are multiple elements, the lists of nodes corresponding to each - podAffinityTerm are intersected, i.e. all terms must be satisfied. - items: - description: |- - Defines a set of pods (namely those matching the labelSelector - relative to the given namespace(s)) that this pod should be - co-located (affinity) or not co-located (anti-affinity) with, - where co-located is defined as running on a node whose value of - the label with key matches that of any node on which - a pod of the set of pods is running - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The - requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default). - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default). - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The - requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - x-kubernetes-list-type: atomic - type: object - podAntiAffinity: - description: Describes pod anti-affinity scheduling - rules (e.g. avoid putting this pod in the same node, - zone, etc. as some other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: |- - The scheduler will prefer to schedule pods to nodes that satisfy - the anti-affinity expressions specified by this field, but it may choose - a node that violates one or more of the expressions. The node that is - most preferred is the one with the greatest sum of weights, i.e. - for each node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling anti-affinity expressions, etc.), - compute a sum by iterating through the elements of this field and adding - "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the - node(s) with the highest sum are the most preferred. - items: - description: The weights of all of the matched - WeightedPodAffinityTerm fields are added per-node - to find the most preferred node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, - associated with the corresponding weight. - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a - list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label - key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default). - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default). - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a - list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label - key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: |- - weight associated with matching the corresponding podAffinityTerm, - in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - x-kubernetes-list-type: atomic - requiredDuringSchedulingIgnoredDuringExecution: - description: |- - If the anti-affinity requirements specified by this field are not met at - scheduling time, the pod will not be scheduled onto the node. - If the anti-affinity requirements specified by this field cease to be met - at some point during pod execution (e.g. due to a pod label update), the - system may or may not try to eventually evict the pod from its node. - When there are multiple elements, the lists of nodes corresponding to each - podAffinityTerm are intersected, i.e. all terms must be satisfied. - items: - description: |- - Defines a set of pods (namely those matching the labelSelector - relative to the given namespace(s)) that this pod should be - co-located (affinity) or not co-located (anti-affinity) with, - where co-located is defined as running on a node whose value of - the label with key matches that of any node on which - a pod of the set of pods is running - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The - requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default). - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default). - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The - requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - x-kubernetes-list-type: atomic - type: object - type: object appServers: items: properties: @@ -8982,327 +8051,8 @@ spec: type: array enabled: type: boolean - frontendPort: - default: 80 - format: int32 - type: integer - image: - default: haproxytech/haproxy-alpine:3.2 - type: string - imagePullSecrets: - items: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - type: array - ingress: - properties: - additionalHosts: - items: - description: |- - IngressRule represents the rules mapping the paths under a specified host to - the related backend services. Incoming requests are first evaluated for a host - match, then routed to the backend associated with the matching IngressRuleValue. - properties: - host: - description: "host is the fully qualified domain - name of a network host, as defined by RFC 3986.\nNote - the following deviations from the \"host\" part - of the\nURI as defined in RFC 3986:\n1. IPs - are not allowed. Currently an IngressRuleValue - can only apply to\n the IP in the Spec of - the parent Ingress.\n2. The `:` delimiter is - not respected because ports are not allowed.\n\t - \ Currently the port of an Ingress is implicitly - :80 for http and\n\t :443 for https.\nBoth - these may change in the future.\nIncoming requests - are matched against the host before the\nIngressRuleValue. - If the host is unspecified, the Ingress routes - all\ntraffic based on the specified IngressRuleValue.\n\nhost - can be \"precise\" which is a domain name without - the terminating dot of\na network host (e.g. - \"foo.bar.com\") or \"wildcard\", which is a - domain name\nprefixed with a single wildcard - label (e.g. \"*.foo.com\").\nThe wildcard character - '*' must appear by itself as the first DNS label - and\nmatches only a single label. You cannot - have a wildcard label by itself (e.g. Host == - \"*\").\nRequests will be matched against the - Host field in the following way:\n1. If host - is precise, the request matches this rule if - the http host header is equal to Host.\n2. If - host is a wildcard, then the request matches - this rule if the http host header\nis to equal - to the suffix (removing the first label) of - the wildcard rule." - type: string - http: - description: |- - HTTPIngressRuleValue is a list of http selectors pointing to backends. - In the example: http:///? -> backend where - where parts of the url correspond to RFC 3986, this resource will be used - to match against everything after the last '/' and before the first '?' - or '#'. - properties: - paths: - description: paths is a collection of paths - that map requests to backends. - items: - description: |- - HTTPIngressPath associates a path with a backend. Incoming urls matching the - path are forwarded to the backend. - properties: - backend: - description: |- - backend defines the referenced service endpoint to which the traffic - will be forwarded to. - properties: - resource: - description: |- - resource is an ObjectRef to another Kubernetes resource in the namespace - of the Ingress object. If resource is specified, a service.Name and - service.Port must not be specified. - This is a mutually exclusive setting with "Service". - properties: - apiGroup: - description: |- - APIGroup is the group for the resource being referenced. - If APIGroup is not specified, the specified Kind must be in the core API group. - For any other third-party types, APIGroup is required. - type: string - kind: - description: Kind is the type - of resource being referenced - type: string - name: - description: Name is the name - of resource being referenced - type: string - required: - - kind - - name - type: object - x-kubernetes-map-type: atomic - service: - description: |- - service references a service as a backend. - This is a mutually exclusive setting with "Resource". - properties: - name: - description: |- - name is the referenced service. The service must exist in - the same namespace as the Ingress object. - type: string - port: - description: |- - port of the referenced service. A port name or port number - is required for a IngressServiceBackend. - properties: - name: - description: |- - name is the name of the port on the Service. - This is a mutually exclusive setting with "Number". - type: string - number: - description: |- - number is the numerical port number (e.g. 80) on the Service. - This is a mutually exclusive setting with "Name". - format: int32 - type: integer - type: object - x-kubernetes-map-type: atomic - required: - - name - type: object - type: object - path: - description: |- - path is matched against the path of an incoming request. Currently it can - contain characters disallowed from the conventional "path" part of a URL - as defined by RFC 3986. Paths must begin with a '/' and must be present - when using PathType with value "Exact" or "Prefix". - type: string - pathType: - description: |- - pathType determines the interpretation of the path matching. PathType can - be one of the following values: - * Exact: Matches the URL path exactly. - * Prefix: Matches based on a URL path prefix split by '/'. Matching is - done on a path element by element basis. A path element refers is the - list of labels in the path split by the '/' separator. A request is a - match for path p if every p is an element-wise prefix of p of the - request path. Note that if the last element of the path is a substring - of the last element in request path, it is not a match (e.g. /foo/bar - matches /foo/bar/baz, but does not match /foo/barbaz). - * ImplementationSpecific: Interpretation of the Path matching is up to - the IngressClass. Implementations can treat this as a separate PathType - or treat it identically to Prefix or Exact path types. - Implementations are required to support all path types. - type: string - required: - - backend - - pathType - type: object - type: array - x-kubernetes-list-type: atomic - required: - - paths - type: object - type: object - type: array - annotations: - additionalProperties: - type: string - type: object - enabled: - default: false - type: boolean - host: - type: string - ingressClassName: - type: string - labels: - additionalProperties: - type: string - type: object - tls: - items: - description: IngressTLS describes the transport layer - security associated with an ingress. - properties: - hosts: - description: |- - hosts is a list of hosts included in the TLS certificate. The values in - this list must match the name/s used in the tlsSecret. Defaults to the - wildcard host setting for the loadbalancer controller fulfilling this - Ingress, if left unspecified. - items: - type: string - type: array - x-kubernetes-list-type: atomic - secretName: - description: |- - secretName is the name of the secret used to terminate TLS traffic on - port 443. Field is left optional to allow TLS routing based on SNI - hostname alone. If the SNI host in a listener conflicts with the "Host" - header field used by an IngressRule, the SNI host is used for termination - and value of the "Host" header is used for routing. - type: string - type: object - type: array - type: object - nodeSelector: - additionalProperties: - type: string - type: object pathBasedRouting: - default: false type: boolean - replicas: - default: 1 - format: int32 - type: integer - resources: - description: ResourceRequirements describes the compute - resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in - PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - service: - description: Service Type string describes ingress methods - for a service - type: string - stats: - default: - auth: - enabled: false - password: "" - username: "" - enabled: false - port: 1024 - properties: - auth: - properties: - enabled: - type: boolean - password: - type: string - username: - type: string - type: object - enabled: - type: boolean - port: - format: int32 - type: integer - type: object tcpPorts: properties: enabled: @@ -9323,35 +8073,6 @@ spec: type: object type: array type: object - timeout: - default: - client: 600 - connect: 600 - server: 600 - properties: - client: - format: int32 - type: integer - connect: - format: int32 - type: integer - server: - format: int32 - type: integer - type: object - tls: - default: - certFileName: "" - enabled: false - secretName: "" - properties: - certFileName: - type: string - enabled: - type: boolean - secretName: - type: string - type: object type: object hugePages: properties: diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index 1cd58dd..2de60e7 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -11,6 +11,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) +// effectiveHAProxyConfig represents the resolved configuration after merging cluster and group settings +type effectiveHAProxyConfig struct { + AppServers []marklogicv1.AppServers + PathBasedRouting *bool + TcpPorts *marklogicv1.Tcpports +} + type HAProxyTemplate struct { FrontendName string BackendName string @@ -84,20 +91,17 @@ func generateHAProxyConfig(ctx context.Context, cr *marklogicv1.MarklogicCluster config.IsPathBased = true } - groupHAConfig := group.HAProxy - - if groupHAConfig == nil { - groupHAConfig = cr.Spec.HAProxy - } + // Create effective configuration by merging cluster and group settings + effectiveConfig := createEffectiveHAProxyConfig(cr.Spec.HAProxy, group.HAProxy) // process tcp ports - if groupHAConfig.TcpPorts != nil && groupHAConfig.TcpPorts.Enabled { + if effectiveConfig.TcpPorts != nil && effectiveConfig.TcpPorts.Enabled { tcpPorts := []marklogicv1.TcpPort{} if cr.Spec.HAProxy.TcpPorts != nil { tcpPorts = cr.Spec.HAProxy.TcpPorts.Ports } - if groupHAConfig.TcpPorts != nil { - tcpPorts = groupHAConfig.TcpPorts.Ports + if effectiveConfig.TcpPorts != nil { + tcpPorts = effectiveConfig.TcpPorts.Ports } for _, tcpPort := range tcpPorts { targetPort := int(tcpPort.TargetPort) @@ -135,10 +139,10 @@ func generateHAProxyConfig(ctx context.Context, cr *marklogicv1.MarklogicCluster } // process http ports with appServers - appServers := groupHAConfig.AppServers + appServers := effectiveConfig.AppServers groupPathBased := *cr.Spec.HAProxy.PathBasedRouting - if groupHAConfig.PathBasedRouting != nil { - groupPathBased = *groupHAConfig.PathBasedRouting + if effectiveConfig.PathBasedRouting != nil { + groupPathBased = *effectiveConfig.PathBasedRouting } if len(appServers) == 0 { appServers = defaultAppServer @@ -418,4 +422,26 @@ func parseTemplateToString(templateStr string, data interface{}) string { return buf.String() } -type Servers []marklogicv1.AppServers +// createEffectiveHAProxyConfig merges cluster-level HAProxy config with group-level overrides +func createEffectiveHAProxyConfig(clusterConfig *marklogicv1.HAProxy, groupConfig *marklogicv1.HAProxyGroup) *effectiveHAProxyConfig { + effective := &effectiveHAProxyConfig{ + AppServers: clusterConfig.AppServers, + PathBasedRouting: clusterConfig.PathBasedRouting, + TcpPorts: clusterConfig.TcpPorts, + } + + if groupConfig != nil { + // Override with group-specific settings if provided + if len(groupConfig.AppServers) > 0 { + effective.AppServers = groupConfig.AppServers + } + if groupConfig.PathBasedRouting != nil { + effective.PathBasedRouting = groupConfig.PathBasedRouting + } + if groupConfig.TcpPorts != nil { + effective.TcpPorts = groupConfig.TcpPorts + } + } + + return effective +} From ae4ad32974f33c04342514d188b294c71d117191 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Wed, 8 Oct 2025 00:00:27 -0700 Subject: [PATCH 10/13] Fix bug in TCP config have wrong number of backend servers --- pkg/k8sutil/haProxyHelper.go | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index 2de60e7..beeaca0 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -114,27 +114,16 @@ func generateHAProxyConfig(ctx context.Context, cr *marklogicv1.MarklogicCluster } else { key = fmt.Sprintf("%d-%d", tcpPort.Port, targetPort) } - if _, exists := tcpMap[key]; exists { - tcpMap[key] = append(tcpMap[key], TCPConfig{ - TcpName: key, - Port: int(tcpPort.Port), - TargetPort: targetPort, - PortName: tcpPort.Name, - PodName: group.Name, - Replicas: int(*group.Replicas), - GroupName: group.Name, - }) - } else { - tcpMap[key] = []TCPConfig{{ - TcpName: key, - Port: int(tcpPort.Port), - TargetPort: targetPort, - PortName: tcpPort.Name, - PodName: group.Name, - Replicas: int(*group.Replicas), - GroupName: group.Name, - }} + tcpConfig := TCPConfig{ + TcpName: key, + Port: int(tcpPort.Port), + TargetPort: targetPort, + PortName: tcpPort.Name, + PodName: group.Name, + Replicas: int(*group.Replicas), + GroupName: group.Name, } + tcpMap[key] = append(tcpMap[key], tcpConfig) } } @@ -385,9 +374,8 @@ listen marklogic-TCP-{{.TcpName }} } result += parseTemplateToString(t, data) name := tcpConfigSlice[0].GroupName - groupReplicas := int(tcpConfigSlice[0].Replicas) for _, tcpConfig := range tcpConfigSlice { - for i := 0; i < groupReplicas; i++ { + for i := 0; i < tcpConfig.Replicas; i++ { data := &HAProxyTemplate{ PortNumber: int(tcpConfig.TargetPort), PodName: tcpConfig.PodName, From 145b7d819e780be90d22db917067b9a350c53ef1 Mon Sep 17 00:00:00 2001 From: Romain Winieski Date: Wed, 8 Oct 2025 17:01:36 +0200 Subject: [PATCH 11/13] Update pkg/k8sutil/haProxyHelper.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/k8sutil/haProxyHelper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index beeaca0..15bcc5e 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -363,7 +363,7 @@ func generateTcpConfig(cr *marklogicv1.MarklogicCluster, config *HAProxyConfig) } for _, tcpConfigSlice := range tcpConfigs { t := ` -listen marklogic-TCP-{{.TcpName }} +listen marklogic-TCP-{{.TcpName}} bind :{{ .PortNumber }} {{ .SslCert }} mode tcp balance leastconn` From cb58d64f692c1065299b6529a84f0a9b3e917d3e Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Wed, 8 Oct 2025 07:28:06 -0700 Subject: [PATCH 12/13] update format --- test/e2e/2_marklogic_cluster_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/2_marklogic_cluster_test.go b/test/e2e/2_marklogic_cluster_test.go index f05c448..f021866 100644 --- a/test/e2e/2_marklogic_cluster_test.go +++ b/test/e2e/2_marklogic_cluster_test.go @@ -319,7 +319,7 @@ func TestMarklogicCluster(t *testing.T) { } // Exponential backoff: 1s, 2s, 4s, 8s, 16s time.Sleep(time.Duration(1<<(attempt-1)) * time.Second) - } + } t.Logf("Query datasource response: %s", output) // Verify MarkLogic logs in Grafana using Loki and Fluent Bit if strings.Contains(string(output), "Starting MarkLogic Server") { From d5e0d68b44ed8d8760d95b1f7f793e2554479210 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Thu, 9 Oct 2025 22:26:20 -0700 Subject: [PATCH 13/13] fix bug pointed by Copilot --- pkg/k8sutil/haProxyHelper.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index 15bcc5e..6ede799 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -268,14 +268,15 @@ backend {{ .BackendName }} PortNumber: backends[0].Port, Path: backends[0].Path, } + if backends[0].IsPathBased { + backendTemplate += ` + http-request replace-path {{.Path}}(/)?(.*) /\2` + } result += parseTemplateToString(backendTemplate, data) for _, backend := range backends { name := backend.GroupName groupReplicas := backend.Replicas - if backend.IsPathBased { - backendTemplate += ` - http-request replace-path {{.Path}}(/)?(.*) /\2` - } + for i := 0; i < groupReplicas; i++ { data := &HAProxyTemplate{ PortNumber: backend.TargetPort,