diff --git a/api/v1/common_types.go b/api/v1/common_types.go index f0d6888..c0a1831 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: ""} @@ -112,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"` @@ -138,9 +145,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/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 8ab1994..6903cf6 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 @@ -164,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 @@ -813,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 9d27b2f..7b2f3a8 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 @@ -4544,6 +4542,9 @@ spec: port: format: int32 type: integer + targetPort: + format: int32 + type: integer type: type: string type: object @@ -8028,1283 +8029,31 @@ 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: - name: - type: string - path: - type: string - port: - format: int32 - type: integer - targetPort: - format: int32 - type: integer - type: - type: string - type: object - 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: - default: - enabled: false + appServers: + items: + properties: + name: + type: string + path: + type: string + port: + format: int32 + type: integer + targetPort: + format: int32 + type: integer + type: + type: string + type: object + type: array + enabled: + type: boolean + pathBasedRouting: + type: boolean + tcpPorts: properties: enabled: type: boolean @@ -9316,40 +8065,14 @@ spec: port: format: int32 type: integer + targetPort: + format: int32 + type: integer type: type: string 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/haProxy.go b/pkg/k8sutil/haProxy.go index 19e3cb2..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,16 +197,16 @@ 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(ctx, 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) } - if cr.Spec.HAProxy.TcpPorts.Enabled { - haProxyData["haproxy.cfg"] += generateTcpConfig(cr) + "\n" - } + haProxyData["haproxy.cfg"] += generateTcpConfig(cr, haproxyConfig) + "\n" return haProxyData } @@ -231,7 +232,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(cc.Ctx, cr)), }, }, Spec: corev1.PodSpec{ diff --git a/pkg/k8sutil/haProxyHelper.go b/pkg/k8sutil/haProxyHelper.go index 1212928..6ede799 100644 --- a/pkg/k8sutil/haProxyHelper.go +++ b/pkg/k8sutil/haProxyHelper.go @@ -2,12 +2,26 @@ package k8sutil import ( "bytes" + "context" + "fmt" + "strings" "text/template" marklogicv1 "github.com/marklogic/marklogic-operator-kubernetes/api/v1" + "sigs.k8s.io/controller-runtime/pkg/log" ) -type HAProxyTemplateData struct { +// 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 + TcpName string TargetPortNumber int PortNumber int PortName string @@ -19,19 +33,169 @@ type HAProxyTemplateData struct { ClusterName string SslCert string sslEnabledServer bool + IsPathBased bool +} + +type HAProxyConfig struct { + IsPathBased bool + FrontEndConfigMap map[string]FrontEndConfig + BackendConfigMap map[string][]BackendConfig + TCPConfigMap map[string][]TCPConfig +} + +type FrontEndConfig struct { + FrontendName string + IsPathBased bool + Port int + TargetPort int + Path string + BackendName string +} + +type BackendConfig struct { + BackendName string + IsPathBased bool + GroupName string + Port int + TargetPort int + Path string + Replicas int +} + +type TCPConfig struct { + TcpName string + Port int + TargetPort int + PortName string + PodName string + Replicas int + GroupName string +} + +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) + tcpMap := make(map[string][]TCPConfig) + 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 + } + + if !config.IsPathBased && group.HAProxy != nil && group.HAProxy.PathBasedRouting != nil && *group.HAProxy.PathBasedRouting == true { + config.IsPathBased = true + } + + // Create effective configuration by merging cluster and group settings + effectiveConfig := createEffectiveHAProxyConfig(cr.Spec.HAProxy, group.HAProxy) + + // process tcp ports + if effectiveConfig.TcpPorts != nil && effectiveConfig.TcpPorts.Enabled { + tcpPorts := []marklogicv1.TcpPort{} + if cr.Spec.HAProxy.TcpPorts != nil { + tcpPorts = cr.Spec.HAProxy.TcpPorts.Ports + } + if effectiveConfig.TcpPorts != nil { + tcpPorts = effectiveConfig.TcpPorts.Ports + } + 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) + } + 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) + } + } + + // process http ports with appServers + appServers := effectiveConfig.AppServers + groupPathBased := *cr.Spec.HAProxy.PathBasedRouting + if effectiveConfig.PathBasedRouting != nil { + groupPathBased = *effectiveConfig.PathBasedRouting + } + 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 !groupPathBased { + if int(appServer.Port) == targetPort { + key = fmt.Sprintf("%d", appServer.Port) + } else { + key = fmt.Sprintf("%d-%d", appServer.Port, targetPort) + } + } else { + pathWithoutSlashes := strings.ReplaceAll(appServer.Path, "/", "") + key = fmt.Sprintf("%d-%s-path", appServer.TargetPort, pathWithoutSlashes) + } + + backendName := "marklogic-" + key + "-backend" + // 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, + IsPathBased: groupPathBased, + 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), + IsPathBased: groupPathBased, + } + backendMap[key] = append(backendMap[key], backend) + } + } + config.FrontEndConfigMap = frontendMap + config.BackendConfigMap = backendMap + config.TCPConfigMap = tcpMap + 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 - if *pathBasedRouting { + if config.IsPathBased { + // front end configuration for path based routing frontEndDef = ` frontend marklogic-pathbased-frontend mode http @@ -39,46 +203,55 @@ 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{ - PortNumber: int(appServer.Port), - TargetPortNumber: int(appServer.TargetPort), - Path: appServer.Path, + for _, backends := range config.BackendConfigMap { + 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) } - } else { - frontEndDef = ` -frontend marklogic-{{ .PortNumber}} + } + // 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 marklogic-{{ .PortNumber}}-backend` - - for _, appServer := range appServers { - data = &HAProxyTemplateData{ - PortNumber: int(appServer.Port), - SslCert: getSSLConfig(cr.Spec.HAProxy.Tls), - } - result += parseTemplateToString(frontEndDef, data) + "\n" + 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" } return result } // generates backend config for HAProxy depending on pathBasedRouting flag and appServers -func generateBackendConfig(cr *marklogicv1.MarklogicCluster) string { - - pathBasedRouting := cr.Spec.HAProxy.PathBasedRouting +func generateBackendConfig(cr *marklogicv1.MarklogicCluster, config *HAProxyConfig) string { + backendConfigs := config.BackendConfigMap var result string backendTemplate := ` -backend marklogic-{{ .PortNumber}}-backend +backend {{ .BackendName }} mode http balance leastconn option forwardfor @@ -89,32 +262,26 @@ backend marklogic-{{ .PortNumber}}-backend stick match req.cook(HostId) stick match req.cook(SessionId) default-server check` - - if *pathBasedRouting { - backendTemplate += ` + for _, backends := range backendConfigs { + data := &HAProxyTemplate{ + BackendName: backends[0].BackendName, + PortNumber: backends[0].Port, + Path: backends[0].Path, + } + if backends[0].IsPathBased { + 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, } 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.TargetPort, PodName: name, - Path: appServer.Path, + Path: backend.Path, Index: i, ServiceName: name, NSName: cr.ObjectMeta.Namespace, @@ -130,7 +297,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 { @@ -140,9 +307,9 @@ 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}}/ }` + 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}}` @@ -156,7 +323,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) @@ -189,30 +356,30 @@ 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 := &HAProxyTemplateData{ - PortNumber: int(tcpPort.Port), + data := &HAProxyTemplate{ + 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 - } - for i := 0; i < groupReplicas; i++ { - data := &HAProxyTemplateData{ - PortNumber: int(tcpPort.Port), - PodName: name, + name := tcpConfigSlice[0].GroupName + for _, tcpConfig := range tcpConfigSlice { + for i := 0; i < tcpConfig.Replicas; i++ { + data := &HAProxyTemplate{ + PortNumber: int(tcpConfig.TargetPort), + PodName: tcpConfig.PodName, Index: i, ServiceName: name, NSName: cr.ObjectMeta.Namespace, @@ -222,7 +389,6 @@ listen marklogic-TCP-{{.PortNumber}} } } } - return result } @@ -245,22 +411,26 @@ func parseTemplateToString(templateStr string, data interface{}) string { return buf.String() } -type Servers []marklogicv1.AppServers - -func getPathList(servers Servers) []string { - var paths []string - for _, server := range servers { - paths = append(paths, server.Path) +// 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, } - 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) + 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 Replicas + + return effective } 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") {