diff --git a/charts/redpanda/configmap.tpl.go b/charts/redpanda/configmap.tpl.go index bf3f770b6c..6211c1b0a0 100644 --- a/charts/redpanda/configmap.tpl.go +++ b/charts/redpanda/configmap.tpl.go @@ -62,3 +62,114 @@ func RedpandaAdditionalStartFlags(dot *helmette.Dot, smp, memory, reserveMemory return append(flags, values.Statefulset.AdditionalRedpandaCmdFlags...) } + +// func RedpandaYAMLKafkaListeners(dot *helmette.Dot) []KafkaListener { +// values := helmette.Unwrap[Values](dot.Values) +// +// input := values.Listeners.Kafka +// +// if input.AuthenticationMethod == "" { +// input.AuthenticationMethod = "sasl" // ?? +// // {{- if or (include "sasl-enabled" $root | fromJson).bool $kafkaService.authenticationMethod }} +// // authentication_method: {{ default "sasl" $kafkaService.authenticationMethod }} +// // {{- end }} +// } +// +// internalCert, ok := values.TLS.Certs[input.TLS.Cert] +// if !ok { +// panic(fmt.Sprintf("referenced certificate not defined: %q", input.TLS.Cert)) +// } +// +// listeners := []KafkaListener{ +// { +// Name: "internal", +// Address: "0.0.0.0", +// Port: values.Listeners.Kafka.Port, +// AuthenticationMethod: &values.Listeners.Kafka.AuthenticationMethod, +// TLS: KafkaListenerTLS{ +// Enabled: false, +// CertFile: fmt.Sprintf("/etc/tls/certs/%s/tls.crt", input.TLS.Cert), +// KeyFile: fmt.Sprintf("/etc/tls/certs/%s/tls.key", input.TLS.Cert), +// RequireClientAuth: *input.TLS.RequireClientAuth, +// }, +// }, +// } +// +// // This is a required field so we use the default in the redpanda debian container. +// defaultTrustStore := "/etc/ssl/certs/ca-certificates.crt" +// +// if internalCert.CAEnabled { +// listeners[0].TLS.TrustStoreFile = fmt.Sprintf("/etc/tls/certs/%s/ca.crt", input.TLS.Cert) +// } else { +// listeners[0].TLS.TrustStoreFile = defaultTrustStore +// } +// +// names := helmette.Keys(input.External) +// helmette.SortAlpha(names) +// +// for _, name := range names { +// listener := input.External[name] +// +// var tls KafkaListenerTLS +// if listener.TLS != nil && listener.TLS.Cert != "" { +// cert, ok := values.TLS.Certs[listener.TLS.Cert] +// if !ok { +// panic("todo") +// } +// +// tls = KafkaListenerTLS{ +// Enabled: *listener.Enabled, +// CertFile: fmt.Sprintf("/etc/tls/certs/%s/tls.crt", listener.TLS.Cert), +// KeyFile: fmt.Sprintf("/etc/tls/certs/%s/tls.key", listener.TLS.Cert), +// RequireClientAuth: *listener.TLS.RequireClientAuth, +// } +// +// if cert.CAEnabled { +// tls.TrustStoreFile = fmt.Sprintf("/etc/tls/certs/%s/ca.crt", listener.TLS.Cert) +// } +// } +// +// listeners = append(listeners, KafkaListener{ +// Name: name, +// Address: "0.0.0.0", +// Port: listener.Port, +// // AdvertisedAddress: , +// TLS: tls, +// }) +// } +// +// return listeners +// } +// +// func RedpandaYAMLListenersKafkaAPI(dot *helmette.Dot) []map[string]any { +// kafkaListeners := RedpandaYAMLKafkaListeners(dot) +// +// var kafka_api []map[string]any +// for _, listener := range kafkaListeners { +// kafka_api = append(kafka_api, map[string]any{ +// "name": listener.Name, +// "address": listener.Address, +// "port": listener.Port, +// "authentication_method": listener.AuthenticationMethod, +// }) +// } +// +// return kafka_api +// } +// +// func RedpandaYAMLListenersKafkaAPITLS(dot *helmette.Dot) []map[string]any { +// kafkaListeners := RedpandaYAMLKafkaListeners(dot) +// +// var kafka_api_tls []map[string]any +// for _, listener := range kafkaListeners { +// kafka_api_tls = append(kafka_api_tls, map[string]any{ +// "name": listener.Name, +// "enabled": listener.TLS.Enabled, +// "cert_file": listener.TLS.CertFile, +// "key_file": listener.TLS.KeyFile, +// "trust_store_file": listener.TLS.TrustStoreFile, +// }) +// } +// +// return kafka_api_tls +// } diff --git a/charts/redpanda/kafka_listener.go b/charts/redpanda/kafka_listener.go new file mode 100644 index 0000000000..e2c56c5e89 --- /dev/null +++ b/charts/redpanda/kafka_listener.go @@ -0,0 +1,119 @@ +package redpanda + +// Commenting conventions: +// {GoFieldName} User facing documentation +// --- +// Developer facing documentation +// Annotations +kubebuilder,+gotohelm,etc + +type BrokerConfig struct { + // KafkaListeners is an amalgam of `kafka_api`, `kafka_api_tls`, + // `advertised_kafka_api`. That may be configured in a more ergonomic and + // less brittle fashion. `{,advertised_}kafka_api{,_tls}` may be set + // directly but are mutually exclusive to KafkaListeners. + KafkaListeners []KafkaListener `json:"kafka_listeners,omitempty"` + // KafkaAPI is a direct mapping to `kafka_api`. It takes precedence over + // everything else, if provided. It is recommend to instead use .KafkaListeners. + // +verbatim + KafkaAPI []map[string]any `json:"kafka_api,omitempty"` + // AdvertisedKafkaAPI is a direct mapping to `advertised_kafka_api`. It + // takes precedence over everything else, if provided. It is recommend to + // instead use .KafkaListeners. + // +verbatim + AdvertisedKafkaAPI []map[string]any `json:"advertised_kafka_api,omitempty"` + // KafkaAPITLS is a direct mapping to `kafka_api_tls`. It takes precedence + // over everything else, if provided. It is recommend to instead use + // .KafkaListeners. + // +verbatim + KafkaAPITLS []map[string]any `json:"kafka_api_tls,omitempty"` +} + +// --- +// Enum definition: https://github.com/redpanda-data/redpanda/blob/8d5d1cc6d56d77b575f69150140aa5689bb75c47/src/v/config/broker_authn_endpoint.h#L30 +// +kubebuilder:validation:Enum=none;sasl;mtls_identity +type KafkaAuthenticationMethod string + +// See also: https://docs.redpanda.com/current/manage/security/listener-configuration/ +type KafkaListener struct { + // Name maps to the `kafka_api[*].name` field. + // Must be unique across all kafka listeners. + // +kubebuilder:validation:MaxLength=15 + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Port maps to the `kafka_api[*].port` field. + // Must be unique across all listeners. + // +kubebuilder:validation:Minimum=1 + Port int32 `json:"port"` + // Address maps to the `kafka_api[*].address` field. + // Generally doesn't need to be set. + // +kubebuilder:default="0.0.0.0" + Address *string `json:"address"` + // AuthenticationMethod maps to the `kafka_api[*].authentication_method` + // field. + // +kubebuilder:default=none + AuthenticationMethod KafkaAuthenticationMethod + // TLS, if set, configures the corresponding `kafka_api_tls` element for this + // `kafka_api` element. + // TLS *KafkaListenerTLS + TLS *KafkaListenerTLS + // AdvertisedAddress, if set, maps to the `advertised_kafka_api[*].address` + // field. + AdvertisedAddress *PerBrokerValue[string] + // AdvertisedPort, if set, maps to the `advertised_kafka_api[*].port` + // field. + AdvertisedPort *PerBrokerValue[int32] +} + +type KafkaListenerTLS struct { + Config *KafkaListenerTLSConfig + TLSCertRef *TLSCertReference + // TODO Nice to have, just provide a reference to a cert-manager reference + // and we'll configure it for you. + // CertificateRef *CertificateReference +} + +// See also https://docs.redpanda.com/current/manage/security/encryption/#configure-tls +type KafkaListenerTLSConfig struct { + // Enabled controls the `kafka_api_tls[*].enabled` field. It is passed + // through verbatim and does not affect any other configuration values. + // +kubebuilder:default=true + // +verbatim + Enabled *bool + + // KeyFile maps to the `kafka_api_tls[*].key_file` field. + KeyFile FileSource + + // CertFile maps to the `kafka_api_tls[*].cert_file` field. + CertFile FileSource + + // TrustStoreFile maps to the `kafka_api_tls[*].truststore_file` field. + // +kubebuilder:default={"path": "/etc/ssl/certs/ca-certificates.crt"} + TrustStoreFile *FileSource + + // RequireClientAuth maps to the `kafka_api_tls[*].required_client_auth` field. + RequireClientAuth bool `json:"required_client_auth"` +} + +func (l *KafkaListener) ToKafkaAPI() map[string]any { + return map[string]any{ + "name": l.Name, + "port": l.Port, + "address": l.Address, + } +} + +func (l *KafkaListener) ToKafkaAPITLS() map[string]any { + return map[string]any{ + "name": l.Name, + "port": l.Port, + "address": l.Address, + } +} + +func (l *KafkaListener) ToAdvertisedKafkaAPI() map[string]any { + return map[string]any{ + "name": l.Name, + "port": l.Port, + "address": l.Address, + } +} diff --git a/charts/redpanda/templates/_configmap.tpl b/charts/redpanda/templates/_configmap.tpl index 04e4b642ea..02023d6722 100644 --- a/charts/redpanda/templates/_configmap.tpl +++ b/charts/redpanda/templates/_configmap.tpl @@ -283,66 +283,8 @@ redpanda.yaml: | {{- end }} {{- end -}} {{/* Kafka API */}} -{{- $kafkaService := .Values.listeners.kafka }} - kafka_api: - - name: internal - address: 0.0.0.0 - port: {{ $kafkaService.port }} -{{- if or (include "sasl-enabled" $root | fromJson).bool $kafkaService.authenticationMethod }} - authentication_method: {{ default "sasl" $kafkaService.authenticationMethod }} -{{- end }} -{{- range $name, $listener := $kafkaService.external }} - {{- if and $listener.port $name (dig "enabled" true $listener) }} - - name: {{ $name }} - address: 0.0.0.0 - port: {{ $listener.port }} - {{- if or (include "sasl-enabled" $root | fromJson).bool $listener.authenticationMethod }} - authentication_method: {{ default "sasl" $listener.authenticationMethod }} - {{- end }} - {{- end }} -{{- end }} - kafka_api_tls: -{{- if (include "kafka-internal-tls-enabled" . | fromJson).bool }} - - name: internal - enabled: true - cert_file: /etc/tls/certs/{{ $kafkaService.tls.cert }}/tls.crt - key_file: /etc/tls/certs/{{ $kafkaService.tls.cert }}/tls.key - require_client_auth: {{ $kafkaService.tls.requireClientAuth }} - {{- $cert := get .Values.tls.certs $kafkaService.tls.cert }} - {{- if empty $cert }} - {{- fail (printf "Certificate used but not defined")}} - {{- end }} - {{- if $cert.caEnabled }} - truststore_file: /etc/tls/certs/{{ $kafkaService.tls.cert }}/ca.crt - {{- else }} - {{/* This is a required field so we use the default in the redpanda debian container */}} - truststore_file: /etc/ssl/certs/ca-certificates.crt - {{- end }} -{{- end }} -{{- range $name, $listener := $kafkaService.external }} - {{- $k := dict "Values" $values "listener" $listener }} - {{- if and (include "kafka-external-tls-enabled" $k | fromJson).bool (dig "enabled" true $listener) }} - {{- $mtls := dig "tls" "requireClientAuth" false $listener }} - {{- $mtls = dig "tls" "requireClientAuth" $mtls $k }} - {{- $certName := include "kafka-external-tls-cert" $k }} - {{- $certPath := printf "/etc/tls/certs/%s" $certName }} - {{- $cert := get $values.tls.certs $certName }} - {{- if empty $cert }} - {{- fail (printf "Certificate, '%s', used but not defined" $certName)}} - {{- end }} - - name: {{ $name }} - enabled: true - cert_file: {{ $certPath }}/tls.crt - key_file: {{ $certPath }}/tls.key - require_client_auth: {{ $mtls }} - {{- if $cert.caEnabled }} - truststore_file: {{ $certPath }}/ca.crt - {{- else }} - {{/* This is a required field so we use the default in the redpanda debian container */}} - truststore_file: /etc/ssl/certs/ca-certificates.crt - {{- end }} - {{- end }} -{{- end -}} + kafka_api: {{ (include "redpanda.RedpandaYAMLKafkaAPIListeners" ) }} + kafka_api_tls: {{ (include "redpanda.RedpandaYAMLKafkaAPITLSListeners" ) }} {{/* RPC Server */}} {{- $service = .Values.listeners.rpc }} rpc_server: diff --git a/charts/redpanda/values.go b/charts/redpanda/values.go index 942747ba67..371b77fa9e 100644 --- a/charts/redpanda/values.go +++ b/charts/redpanda/values.go @@ -17,6 +17,11 @@ import ( // the Values struct as well to ensure that nothing can ever get out of sync. type Values struct { + // --- + // RedpandaYAML represents redpanda.yaml as is read by redpanda. Provide + // direct access to every field. Enhance when possible. + RedpandaYAML `json:",inline"` + NameOverride string `json:"nameOverride"` FullnameOverride string `json:"fullnameOverride"` ClusterDomain string `json:"clusterDomain"` @@ -54,6 +59,10 @@ type Values struct { } `json:"tests"` } +type RedpandaYAML struct { + Redpanda *BrokerConfig `json:"redpanda"` +} + func (Values) JSONSchemaExtend(schema *jsonschema.Schema) { deprecate(schema, "license_key", "license_secret_ref") } @@ -121,8 +130,8 @@ type Auth struct { } type TLS struct { - Enabled *bool `json:"enabled" jsonschema:"required"` - Certs *TLSCertMap `json:"certs"` + Enabled *bool `json:"enabled" jsonschema:"required"` + Certs TLSCertMap `json:"certs"` } type ExternalConfig struct { @@ -487,7 +496,7 @@ type KafkaListeners struct { AuthenticationMethod string `json:"authenticationMethod" jsonschema:"pattern=sasl|none|mtls_identity"` External ExternalListeners[KafkaExternal] `json:"external"` TLS *ExternalTLS `json:"tls" jsonschema:"required"` - Port int `json:"port" jsonschema:"required"` + Port int32 `json:"port" jsonschema:"required"` } type KafkaExternal struct { diff --git a/charts/redpanda/values_util.go b/charts/redpanda/values_util.go index 434bef7ad1..aa158fbc67 100644 --- a/charts/redpanda/values_util.go +++ b/charts/redpanda/values_util.go @@ -1,12 +1,15 @@ -//+gotohelm:ignore=true +// +gotohelm:ignore=true package redpanda import ( "fmt" "github.com/invopop/jsonschema" + corev1 "k8s.io/api/core/v1" ) +type TLSCertReference string + type ImageTag string func (ImageTag) JSONSchemaExtend(schema *jsonschema.Schema) { @@ -51,3 +54,25 @@ func deprecate(schema *jsonschema.Schema, keys ...string) { prop.Deprecated = true } } + +// FileSource ... +// +kubebuilder:validation:MaxProperties=1 +type FileSource struct { + Path *string + Contents []byte + SecretKeyRef *corev1.SecretKeySelector + ConfigMapRef *corev1.ConfigMapKeySelector +} + +// PerBrokerValue allows configuring a value per Redpanda Broker/Node/Pod. +type PerBrokerValue[T comparable] struct { + // Static, if provided, is a static value that will be set verbatim + // regardless of the broker. + Static *T + // ByOrdinal is a list of values that will be used + // It's length MUST be greater than or equal to (>=) the number of Redpanda + // Brokers. + ByOrdinal *[]T + // TODO decide how this will work. + Template *string +}