Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 80 additions & 49 deletions internal/controller/clickhouse/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import (

var (
//go:embed templates/base.yaml.tmpl
baseConfigTemplateStr string
baseTemplateStr string
//go:embed templates/named_collections.yaml.tmpl
namedCollectionsTemplateStr string
//go:embed templates/network.yaml.tmpl
networkConfigTemplateStr string
//go:embed templates/log_tables.yaml.tmpl
Expand All @@ -33,17 +35,51 @@ var (
)

func init() {
templateFuncs := template.FuncMap{
"yaml": func(v any) (string, error) {
data, err := yaml.Marshal(v)
return string(data), err
},
"indent": func(countRaw any, strRaw any) (string, error) {
count, ok := countRaw.(int)
if !ok {
return "", fmt.Errorf("indent: expected int for indentation value, got %T", countRaw)
}

str, ok := strRaw.(string)
if !ok {
return "", fmt.Errorf("indent: expected string for content value, got %T", strRaw)
}

builder := strings.Builder{}
indentation := strings.Repeat(" ", count)

for line := range strings.SplitSeq(str, "\n") {
if _, err := builder.WriteString(fmt.Sprintf("%s%s\n", indentation, line)); err != nil {
return "", fmt.Errorf("failed to write indented line: %w", err)
}
}

return builder.String(), nil
},
}

baseTmpl := template.Must(template.New("").Funcs(templateFuncs).Parse(baseTemplateStr))

generators = append(generators, &templateConfigGenerator{
path: ConfigPath,
filename: ConfigFileName,
template: baseTmpl,
generator: executeBaseConfig,
})

for _, templateSpec := range []struct {
Path string
Filename string
Raw string
Generator configGeneratorFunc
Enabled func(r *clickhouseReconciler) bool
}{{
Path: ConfigPath,
Filename: ConfigFileName,
Raw: baseConfigTemplateStr,
Generator: baseConfigGenerator,
}, {
Path: path.Join(ConfigPath, ConfigDPath),
Filename: "00-network.yaml",
Raw: networkConfigTemplateStr,
Expand All @@ -53,6 +89,14 @@ func init() {
Filename: "00-logs-tables.yaml",
Raw: logTablesConfigTemplateStr,
Generator: logTablesConfigGenerator,
}, {
Path: path.Join(ConfigPath, ConfigDPath),
Filename: "00-named-collections.yaml",
Raw: namedCollectionsTemplateStr,
Generator: namedCollectionsConfigGenerator,
Enabled: func(r *clickhouseReconciler) bool {
return versionAtLeast(r.Cluster.Status.Version, MinVersionNamedCollections)
},
}, {
Path: ConfigPath,
Filename: UsersFileName,
Expand All @@ -64,68 +108,35 @@ func init() {
Raw: clientConfigTemplateStr,
Generator: clientConfigGenerator,
}} {
tmpl := template.New("").Funcs(template.FuncMap{
"yaml": func(v any) (string, error) {
data, err := yaml.Marshal(v)
return string(data), err
},
"indent": func(countRaw any, strRaw any) (string, error) {
count, ok := countRaw.(int)
if !ok {
return "", fmt.Errorf("indent: expected int for indentation value, got %T", countRaw)
}

str, ok := strRaw.(string)
if !ok {
return "", fmt.Errorf("indent: expected string for content value, got %T", strRaw)
}

builder := strings.Builder{}
indentation := strings.Repeat(" ", count)

for line := range strings.SplitSeq(str, "\n") {
if _, err := builder.WriteString(fmt.Sprintf("%s%s\n", indentation, line)); err != nil {
return "", fmt.Errorf("failed to write indented line: %w", err)
}
}

return builder.String(), nil
},
})
if _, err := tmpl.Parse(templateSpec.Raw); err != nil {
panic(fmt.Sprintf("failed to parse template %s: %v", templateSpec.Filename, err))
}
tmpl := template.Must(template.New("").Funcs(templateFuncs).Parse(templateSpec.Raw))

generators = append(generators, &templateConfigGenerator{
filename: templateSpec.Filename,
path: templateSpec.Path,
template: tmpl,
generator: templateSpec.Generator,
enabled: templateSpec.Enabled,
})
}

generators = append(generators,
&extraConfigGenerator{
Name: ExtraConfigFileName,
ConfigSubPath: ConfigDPath,
Getter: func(r *clickhouseReconciler) []byte {
return r.Cluster.Spec.Settings.ExtraConfig.Raw
},
Getter: func(r *clickhouseReconciler) []byte { return r.Cluster.Spec.Settings.ExtraConfig.Raw },
},
&extraConfigGenerator{
Name: ExtraUsersConfigFileName,
ConfigSubPath: UsersDPath,
Getter: func(r *clickhouseReconciler) []byte {
return r.Cluster.Spec.Settings.ExtraUsersConfig.Raw
},
Getter: func(r *clickhouseReconciler) []byte { return r.Cluster.Spec.Settings.ExtraUsersConfig.Raw },
})
}

type configGenerator interface {
Filename() string
Path() string
ConfigKey() string
Exists(r *clickhouseReconciler) bool
Enabled(r *clickhouseReconciler) bool
Generate(r *clickhouseReconciler, id v1.ClickHouseReplicaID) (string, error)
}

Expand All @@ -134,6 +145,7 @@ type templateConfigGenerator struct {
path string
template *template.Template
generator configGeneratorFunc
enabled func(r *clickhouseReconciler) bool
}

func (g *templateConfigGenerator) Filename() string {
Expand All @@ -148,8 +160,8 @@ func (g *templateConfigGenerator) ConfigKey() string {
return controllerutil.PathToName(path.Join(g.path, g.filename))
}

func (g *templateConfigGenerator) Exists(*clickhouseReconciler) bool {
return true
func (g *templateConfigGenerator) Enabled(r *clickhouseReconciler) bool {
return g.enabled == nil || g.enabled(r)
}

func (g *templateConfigGenerator) Generate(r *clickhouseReconciler, id v1.ClickHouseReplicaID) (string, error) {
Expand Down Expand Up @@ -194,7 +206,7 @@ type keeperNode struct {
Secure bool
}

func baseConfigGenerator(tmpl *template.Template, r *clickhouseReconciler, id v1.ClickHouseReplicaID) (string, error) {
func executeBaseConfig(tmpl *template.Template, r *clickhouseReconciler, id v1.ClickHouseReplicaID) (string, error) {
keeperNodes := make([]keeperNode, 0, r.keeper.Replicas())
for _, host := range r.keeper.Hostnames() {
if r.keeper.Spec.Settings.TLS.Enabled {
Expand Down Expand Up @@ -389,6 +401,25 @@ func clientConfigGenerator(tmpl *template.Template, r *clickhouseReconciler, _ v
return builder.String(), nil
}

type namedCollectionsConfigParams struct {
NamedCollectionsKeyEnv string
NamedCollectionsPath string
}

func namedCollectionsConfigGenerator(tmpl *template.Template, _ *clickhouseReconciler, _ v1.ClickHouseReplicaID) (string, error) {
params := namedCollectionsConfigParams{
NamedCollectionsKeyEnv: EnvNamedCollectionsKey,
NamedCollectionsPath: KeeperPathNamedCollections,
}

builder := strings.Builder{}
if err := tmpl.Execute(&builder, params); err != nil {
return "", fmt.Errorf("template named collections config: %w", err)
}

return builder.String(), nil
}

type extraConfigGenerator struct {
Name string
ConfigSubPath string
Expand All @@ -407,12 +438,12 @@ func (g *extraConfigGenerator) ConfigKey() string {
return g.Name
}

func (g *extraConfigGenerator) Exists(r *clickhouseReconciler) bool {
func (g *extraConfigGenerator) Enabled(r *clickhouseReconciler) bool {
return len(g.Getter(r)) > 0
}

func (g *extraConfigGenerator) Generate(r *clickhouseReconciler, _ v1.ClickHouseReplicaID) (string, error) {
if !g.Exists(r) {
if !g.Enabled(r) {
return "", errors.New("extra config generator called, but no extra config provided")
}

Expand Down
5 changes: 4 additions & 1 deletion internal/controller/clickhouse/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ var _ = Describe("ConfigGenerator", func() {
},
},
},
Status: v1.ClickHouseClusterStatus{
Version: "25.12.1.1",
},
},
},
keeper: v1.KeeperCluster{
Expand All @@ -42,7 +45,7 @@ var _ = Describe("ConfigGenerator", func() {

for _, generator := range generators {
It("should generate config: "+generator.Filename(), func() {
Expect(generator.Exists(&ctx)).To(BeTrue())
Expect(generator.Enabled(&ctx)).To(BeTrue())
data, err := generator.Generate(&ctx, v1.ClickHouseReplicaID{})
Expect(err).ToNot(HaveOccurred())

Expand Down
81 changes: 64 additions & 17 deletions internal/controller/clickhouse/constants.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package clickhouse

import (
"fmt"

"github.com/blang/semver/v4"

v1 "github.com/ClickHouse/clickhouse-operator/api/v1alpha1"
"github.com/ClickHouse/clickhouse-operator/internal/controllerutil"
"github.com/ClickHouse/clickhouse-operator/internal/upgrade"
)

// MinVersionNamedCollections is the minimum ClickHouse version that supports keeper_encrypted for named collections.
var MinVersionNamedCollections = upgrade.ClickHouseVersion{Major: 25, Minor: 12} //nolint:mnd

const (
PortManagement = 9001
PortNative = 9000
Expand Down Expand Up @@ -32,10 +41,11 @@ const (

LogPath = "/var/log/clickhouse-server/"

DefaultClusterName = "default"
KeeperPathUsers = "/clickhouse/access"
KeeperPathUDF = "/clickhouse/user_defined"
KeeperPathDistributedDDL = "/clickhouse/task_queue/ddl"
DefaultClusterName = "default"
KeeperPathUsers = "/clickhouse/access"
KeeperPathUDF = "/clickhouse/user_defined"
KeeperPathDistributedDDL = "/clickhouse/task_queue/ddl"
KeeperPathNamedCollections = "/clickhouse/named_collections"

ContainerName = "clickhouse-server"
DefaultRevisionHistory = 10
Expand All @@ -49,27 +59,64 @@ const (
EnvDefaultUserPassword = "CLICKHOUSE_DEFAULT_USER_PASSWORD"
EnvKeeperIdentity = "CLICKHOUSE_KEEPER_IDENTITY"
EnvClusterSecret = "CLICKHOUSE_CLUSTER_SECRET"
EnvNamedCollectionsKey = "CLICKHOUSE_NAMED_COLLECTIONS_KEY"

SecretKeyInterserverPassword = "interserver-password"
SecretKeyManagementPassword = "management-password"
SecretKeyKeeperIdentity = "keeper-identity"
SecretKeyClusterSecret = "cluster-secret"
SecretKeyNamedCollectionsKey = "named-collections-key"

// NamedCollectionsKeyByteLen is the AES-128 key size in bytes (16 bytes = 32 hex chars).
NamedCollectionsKeyByteLen = 16
)

// versionAtLeast returns true if the actual version string is >= min.
// Returns false for empty, unparsable, or unknown version strings.
func versionAtLeast(actual string, minVersion upgrade.ClickHouseVersion) bool {
v, err := upgrade.ParseBareVersion(actual)
if err != nil {
return false
}

return v.Compare(minVersion) >= 0
}

type secretSpec struct {
Key string
Env string
Format string
Generate func() any
Enabled func(status *v1.ClickHouseCluster) bool
}

func (s *secretSpec) generate() []byte {
var arg any
if s.Generate != nil {
arg = s.Generate()
} else {
arg = controllerutil.GeneratePassword()
}

return fmt.Appendf(nil, s.Format, arg)
}

func (s *secretSpec) enabled(cluster *v1.ClickHouseCluster) bool {
return s.Enabled == nil || s.Enabled(cluster)
}

var (
breakingStatefulSetVersion, _ = semver.Parse("0.0.1")
secretsToGenerate = map[string]string{
SecretKeyInterserverPassword: "%s",
SecretKeyManagementPassword: "%s",
SecretKeyKeeperIdentity: "clickhouse:%s",
SecretKeyClusterSecret: "%s",
}
secretsToEnvMapping = []struct {
Key string
Env string
}{
{Key: SecretKeyInterserverPassword, Env: EnvInterserverPassword},
{Key: SecretKeyKeeperIdentity, Env: EnvKeeperIdentity},
{Key: SecretKeyClusterSecret, Env: EnvClusterSecret},
clusterSecrets = []secretSpec{
{Key: SecretKeyInterserverPassword, Env: EnvInterserverPassword, Format: "%s"},
{Key: SecretKeyManagementPassword, Format: "%s"},
{Key: SecretKeyKeeperIdentity, Env: EnvKeeperIdentity, Format: "clickhouse:%s"},
{Key: SecretKeyClusterSecret, Env: EnvClusterSecret, Format: "%s"},
{Key: SecretKeyNamedCollectionsKey, Env: EnvNamedCollectionsKey, Format: "%x",
Generate: func() any { return controllerutil.GenerateRandomBytes(NamedCollectionsKeyByteLen) },
Enabled: func(status *v1.ClickHouseCluster) bool {
return versionAtLeast(status.Spec.ClusterDomain, MinVersionNamedCollections)
},
},
}
)
15 changes: 12 additions & 3 deletions internal/controller/clickhouse/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ var _ = When("reconciling ClickHouseCluster", Ordered, func() {
"test-annotation": "test-val",
},
},
Status: v1.ClickHouseClusterStatus{
Version: "26.1.1.1",
},
}
)

Expand Down Expand Up @@ -88,6 +91,8 @@ var _ = When("reconciling ClickHouseCluster", Ordered, func() {
Status: metav1.ConditionTrue,
Reason: string(v1.KeeperConditionReasonStandaloneReady),
})
// Unblocks CommonResources (secrets/commander); version-gated ClickHouse config uses ClickHouseCluster.status.version.
keeper.Status.Version = "26.1.1.1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should check configs with ClickHouse version

Expect(suite.Client.Status().Update(ctx, keeper)).To(Succeed())
})

Expand Down Expand Up @@ -225,9 +230,13 @@ var _ = When("reconciling ClickHouseCluster", Ordered, func() {
})

It("should generate all secret values", func() {
for key := range secretsToGenerate {
Expect(secrets.Items[0].Data).To(HaveKey(key))
Expect(secrets.Items[0].Data[key]).To(Not(BeEmpty()))
for _, spec := range clusterSecrets {
if spec.Enabled != nil {
continue
}

Expect(secrets.Items[0].Data).To(HaveKey(spec.Key))
Expect(secrets.Items[0].Data[spec.Key]).To(Not(BeEmpty()))
}
})

Expand Down
Loading
Loading