Skip to content

Commit a6c0672

Browse files
authored
Add a type to the v1 model for the updated ClusterConfig (#489)
* Add a type to the v1 model for the updated ClusterConfig ClusterConfiguration values can either be concrete ones, or references - either to k8s resources (such as ConfigMap or Secret) or an external secret. Concrete items are represented as strings - the attribute in a CR should have the yaml representation of the raw value. This permits us to inject it verbatim into a reconstituted `.bootstrap.yaml` file, and (with the aid of a schema from a running Redpanda cluster) turn the representation into concrete values without loss of fidelity. We use json encoding to construct these, and yaml decoding (which is a little more flexible in what it'll accept) to decode them. * Add the construction of a bootstrap template file This is ugly at the moment - the intention here is to add the bootstrap templating without too much change. We'll follow up with commits to remove swathes of the v1 operator behaviour - such as removing drift detection - after this has landed. There's additional behaviour in the `configure` subcommand, to template-expand environmant variables and/or external secrets. Internally, the operator uses the bootstrap template file to detemine whether reconfiguration should be applied. This is a short-term adjustment - it'll behave correctly on concrete values, but won't necessarily pick up external sources of values (such as config maps) changing. That's to come. The operator takes some care to not unnecessarily trigger a sts restart - if the sts has already been configured without a tempalted bootstrap, then the old behaviour will be preserved until such time as a restart would be required anyway. * kuttl test for "ClusterConfiguration" override Ensure the configuration is correctly stashed and updated. kuttl test for "ClusterConfiguration" external ref override Note: because this test causes an additional env var to be added to an initContainer, it'll cause a restart of the resulting statefulSet.
1 parent bb95984 commit a6c0672

32 files changed

+1101
-66
lines changed

operator/api/vectorized/v1alpha1/cluster_types.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ type ClusterSpec struct {
170170
// By default if Replicas is 3 or more and redpanda.default_topic_partitions is not set
171171
// default webhook is setting redpanda.default_topic_partitions to 3.
172172
AdditionalConfiguration map[string]string `json:"additionalConfiguration,omitempty"`
173+
// Cluster configuration values may also be held here. A `keyName` entry in this
174+
// attribute will override corresponding `redpanda.keyName` entries in AdditionalConfiguration;
175+
// this is to permit the migration of those settings to this attribute.
176+
// The configuration may contain references to values extracted from k8s ConfigMaps or Secrets;
177+
// furthermore, we support the fetching of provider-specific secrets directly.
178+
ClusterConfiguration ClusterConfiguration `json:"clusterConfiguration,omitempty"`
173179
// DNSTrailingDotDisabled gives ability to turn off the fully-qualified
174180
// DNS name.
175181
// http://www.dns-sd.org/trailingdotsindomainnames.html
@@ -196,6 +202,53 @@ type ClusterSpec struct {
196202
NodePools []NodePoolSpec `json:"nodePools,omitempty"`
197203
}
198204

205+
// ClusterConfiguration holds values (or references to values) that should be used
206+
// to configure the cluster. Where the cluster schema defines a non-string type for a
207+
// given key, the corresponding values here should be string-encoded (according to yaml
208+
// rules)
209+
type ClusterConfiguration map[string]ClusterConfigValue
210+
211+
// YAMLRepresentation holds a serialised form of a concrete value. We need this for
212+
// a couple of reasons: firstly, "stringifying" numbers avoids loss of accuracy and
213+
// rendering issues where intermediate values are represented as f64 values by
214+
// external tooling. Secondly, the initial configuration of a bootstrap file has
215+
// no running cluster - and therefore no online schema - available. Instead we use
216+
// representations that can be inserted verbatim into a YAML document.
217+
// Ideally, these will be JSON-encoded into a single line representation. They are
218+
// decoded using YAML deserialisation (which has a little more flexibility around
219+
// the representation of unambiguous string values).
220+
type YAMLRepresentation string
221+
222+
// ClusterConfigValue represents a value of arbitrary type T. Values are string-encoded according to
223+
// YAML rules in order to preserve numerical fidelity.
224+
// Because these values must be embedded in a `.bootstrap.yaml` file - during the processing of
225+
// which, the AdminAPI's schema is unavailable - we endeavour to use yaml-compatible representations
226+
// throughout. The octet sequence of a Representation will be inserted into a bootstrap template
227+
// verbatim.
228+
// TODO: this type should be lifted to a shared module rather than duplicated in the `redpanda` CRD definition.
229+
// TODO: add CEL validation here.
230+
type ClusterConfigValue struct {
231+
// If the value is directly known, its yaml representation can be embedded here.
232+
// Use the string representation of a yaml-serialised value in order to preserve accuracy.
233+
// Example:
234+
// The string "foo" should be the five octets "\"foo\""
235+
// A true value should be the four octets "true".
236+
// The number -123456 should be a seven-octet sequence, "-123456".
237+
Repr *YAMLRepresentation `json:"repr,omitempty"`
238+
// If the value is supplied by an k8s object reference, coordinates are embedded here.
239+
// For target values, the string value fetched from the source will be treated as
240+
// a value encoded according to YAML rules; the string can then be embedded verbatim into
241+
// the bootstrap file.
242+
ConfigMapKeyRef *corev1.ConfigMapKeySelector `json:"configMapKeyRef,omitempty"`
243+
// Should the value be contained in a k8s secret rather than configmap, we can refer
244+
// to it here.
245+
SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"`
246+
// If the value is supplied by an external source, coordinates are embedded here.
247+
// Note: we interpret all fetched external secrets as string values and yam-encode them prior to embedding.
248+
// TODO: This decision needs finalising and documenting.
249+
ExternalSecretRef *string `json:"externalSecretRef,omitempty"`
250+
}
251+
199252
// NodePoolSpec defines a NodePool. NodePools have their own:
200253
// NodeSelector, so they can be scheduled on specific cloud provider Node Pools.
201254
// Storage, as different NodePools may have different disk shapes.

operator/api/vectorized/v1alpha1/zz_generated.deepcopy.go

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package configurator
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"maps"
7+
"os"
8+
"slices"
9+
"strings"
10+
11+
"github.com/redpanda-data/redpanda-operator/operator/pkg/clusterconfiguration"
12+
)
13+
14+
// Template out the bootstrap file
15+
// This takes an input template, resolves any remaining external references, then writes out the resulting bootstrap file
16+
func templateBootstrapYaml(inFile, outFile string) error {
17+
var template map[string]clusterconfiguration.ClusterConfigTemplateValue
18+
buf, err := os.ReadFile(inFile)
19+
if err != nil {
20+
return fmt.Errorf("cannot load bootstrap template file: %w", err)
21+
}
22+
if err := json.Unmarshal(buf, &template); err != nil {
23+
return fmt.Errorf("cannot parse bootstrap template file: %w", err)
24+
}
25+
26+
var config []string
27+
keys := slices.Sorted(maps.Keys(template))
28+
for _, k := range keys {
29+
// Work out what the value should be and add it to the output.
30+
repr, err := clusterconfiguration.ExpandValueForTemplate(template[k])
31+
if err != nil {
32+
return fmt.Errorf("cannot resolve value %s: %w", k, err)
33+
}
34+
config = append(config, fmt.Sprintf("%s: %s", k, repr))
35+
}
36+
37+
output := strings.Join(config, "\n")
38+
return os.WriteFile(outFile, []byte(output), 0o644)
39+
}

operator/cmd/configurator/configurator.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ const (
5959
svcFQDNEnvVar = "SERVICE_FQDN"
6060
additionalListenersLegacyEnvVar = "ADDITIONAL_LISTENERS"
6161
additionalListenersJSONEnvVar = "ADDITIONAL_LISTENERS_JSON"
62+
bootstrapDestinationEnvVar = "BOOTSTRAP_DESTINATION"
63+
bootstrapTemplateEnvVar = "BOOTSTRAP_TEMPLATE"
6264
)
6365

6466
type brokerID int
@@ -83,6 +85,8 @@ type configuratorConfig struct {
8385
additionalListenersLegacy string
8486
additionalListenersJSON string
8587
hostIndexOffset int
88+
bootstrapDestination string
89+
bootstrapTemplate string
8690
}
8791

8892
func (c *configuratorConfig) String() string {
@@ -220,6 +224,14 @@ func run(cmd *cobra.Command, args []string) {
220224
log.Fatalf("%s", fmt.Errorf("unable to write the destination configuration file: %w", err))
221225
}
222226

227+
if c.bootstrapTemplate != "" && c.bootstrapDestination != "" {
228+
// Perform the bootstrap templating
229+
err := templateBootstrapYaml(c.bootstrapTemplate, c.bootstrapDestination)
230+
if err != nil {
231+
log.Fatalf("%s", fmt.Errorf("unable to template the .bootstrap.yaml file: %w", err))
232+
}
233+
}
234+
223235
log.Printf("Configuration saved to: %s", c.configDestination)
224236
}
225237

@@ -582,6 +594,18 @@ func checkEnvVars() (configuratorConfig, error) {
582594
log.Printf("additional listeners configured with the JSON format: %v", c.additionalListenersJSON)
583595
}
584596

597+
// Handling the templating of the bootstrap file is optional
598+
bootstrapTemplate, exist := os.LookupEnv(bootstrapTemplateEnvVar)
599+
if exist && bootstrapTemplate != "" {
600+
bootstrapDestination, exist := os.LookupEnv(bootstrapDestinationEnvVar)
601+
if !exist || bootstrapDestination == "" {
602+
result = errors.Join(result, fmt.Errorf("%s specified without corresponding %s", bootstrapTemplateEnvVar, bootstrapDestinationEnvVar))
603+
} else {
604+
c.bootstrapTemplate = bootstrapTemplate
605+
c.bootstrapDestination = bootstrapDestination
606+
}
607+
}
608+
585609
return c, result
586610
}
587611

operator/config/crd/bases/redpanda.vectorized.io_clusters.yaml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,91 @@ spec:
173173
required:
174174
- enabled
175175
type: object
176+
clusterConfiguration:
177+
additionalProperties:
178+
description: |-
179+
ClusterConfigValue represents a value of arbitrary type T. Values are string-encoded according to
180+
YAML rules in order to preserve numerical fidelity.
181+
Because these values must be embedded in a `.bootstrap.yaml` file - during the processing of
182+
which, the AdminAPI's schema is unavailable - we endeavour to use yaml-compatible representations
183+
throughout. The octet sequence of a Representation will be inserted into a bootstrap template
184+
verbatim.
185+
properties:
186+
configMapKeyRef:
187+
description: |-
188+
If the value is supplied by an k8s object reference, coordinates are embedded here.
189+
For target values, the string value fetched from the source will be treated as
190+
a value encoded according to YAML rules; the string can then be embedded verbatim into
191+
the bootstrap file.
192+
properties:
193+
key:
194+
description: The key to select.
195+
type: string
196+
name:
197+
default: ""
198+
description: |-
199+
Name of the referent.
200+
This field is effectively required, but due to backwards compatibility is
201+
allowed to be empty. Instances of this type with an empty value here are
202+
almost certainly wrong.
203+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
204+
type: string
205+
optional:
206+
description: Specify whether the ConfigMap or its key must
207+
be defined
208+
type: boolean
209+
required:
210+
- key
211+
type: object
212+
x-kubernetes-map-type: atomic
213+
externalSecretRef:
214+
description: |-
215+
If the value is supplied by an external source, coordinates are embedded here.
216+
Note: we interpret all fetched external secrets as string values and yam-encode them prior to embedding.
217+
type: string
218+
repr:
219+
description: |-
220+
If the value is directly known, its yaml representation can be embedded here.
221+
Use the string representation of a yaml-serialised value in order to preserve accuracy.
222+
Example:
223+
The string "foo" should be the five octets "\"foo\""
224+
A true value should be the four octets "true".
225+
The number -123456 should be a seven-octet sequence, "-123456".
226+
type: string
227+
secretKeyRef:
228+
description: |-
229+
Should the value be contained in a k8s secret rather than configmap, we can refer
230+
to it here.
231+
properties:
232+
key:
233+
description: The key of the secret to select from. Must
234+
be a valid secret key.
235+
type: string
236+
name:
237+
default: ""
238+
description: |-
239+
Name of the referent.
240+
This field is effectively required, but due to backwards compatibility is
241+
allowed to be empty. Instances of this type with an empty value here are
242+
almost certainly wrong.
243+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
244+
type: string
245+
optional:
246+
description: Specify whether the Secret or its key must
247+
be defined
248+
type: boolean
249+
required:
250+
- key
251+
type: object
252+
x-kubernetes-map-type: atomic
253+
type: object
254+
description: |-
255+
Cluster configuration values may also be held here. A `keyName` entry in this
256+
attribute will override corresponding `redpanda.keyName` entries in AdditionalConfiguration;
257+
this is to permit the migration of those settings to this attribute.
258+
The configuration may contain references to values extracted from k8s ConfigMaps or Secrets;
259+
furthermore, we support the fetching of provider-specific secrets directly.
260+
type: object
176261
configuration:
177262
description: Configuration represent redpanda specific configuration
178263
properties:

operator/internal/controller/vectorized/cluster_controller_attached_resources.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ func (a *attachedResources) statefulSet() error {
422422
a.getServiceAccountName(),
423423
a.reconciler.configuratorSettings,
424424
cm.GetNodeConfigHash,
425+
cm.CreateConfiguration,
425426
a.reconciler.AdminAPIClientFactory,
426427
a.reconciler.Dialer,
427428
a.reconciler.DecommissionWaitInterval,

0 commit comments

Comments
 (0)