diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index e01a5f997..fb36340e2 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -66,6 +66,9 @@ spec: nullable: true items: type: string + database_name_regexp: + type: string + default: "^[a-zA-Z_][a-zA-Z0-9_]*$" docker_image: type: string default: "ghcr.io/zalando/spilo-15:2.1-p9" diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 198870d77..b1086b14b 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -177,6 +177,12 @@ Those are top-level keys, containing both leaf keys and groups. operator's own container, change the [operator deployment manually](https://github.com/zalando/postgres-operator/blob/master/manifests/postgres-operator.yaml#L20). The default is `false`. + +* **database_name_regexp** + By default the operator will follow the naming conventions for PostgreSQL. However, + the regexp can be modified to customize the validation. + The default is `^[a-zA-Z_][a-zA-Z0-9_]*$`. + ## Postgres users Parameters describing Postgres users. In a CRD-configuration, they are grouped diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index e2fb21504..82923b64e 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -26,6 +26,7 @@ data: crd_categories: "all" # custom_service_annotations: "keyx:valuez,keya:valuea" # custom_pod_annotations: "keya:valuea,keyb:valueb" + # database_name_regexp: "^[a-zA-Z_][a-zA-Z0-9_]*$" db_hosted_zone: db.example.com debug_logging: "true" # default_cpu_limit: "1" diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 8582c866a..5a0875d5c 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -64,6 +64,9 @@ spec: nullable: true items: type: string + database_name_regexp: + type: string + default: "^[a-zA-Z_][a-zA-Z0-9_]*$" docker_image: type: string default: "ghcr.io/zalando/spilo-15:2.1-p9" diff --git a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go index 4ff5ee81e..750b81475 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -3,10 +3,10 @@ package v1 // Operator configuration CRD definition, please use snake_case for field names. import ( - "github.com/zalando/postgres-operator/pkg/util/config" - "time" + "github.com/zalando/postgres-operator/pkg/util/config" + "github.com/zalando/postgres-operator/pkg/spec" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -102,6 +102,7 @@ type KubernetesMetaConfiguration struct { PodManagementPolicy string `json:"pod_management_policy,omitempty"` EnableReadinessProbe bool `json:"enable_readiness_probe,omitempty"` EnableCrossNamespaceSecret bool `json:"enable_cross_namespace_secret,omitempty"` + DatabaseNameRegexp string `json:"database_name_regexp" default:"^[a-zA-Z_][a-zA-Z0-9_]*$"` } // PostgresPodResourcesDefaults defines the spec of default resources diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 590fe6564..80716d276 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -294,6 +294,10 @@ func (c *Cluster) Create() error { c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeNormal, "Services", "The service %q for role %s has been successfully created", util.NameFromMeta(service.ObjectMeta), role) } + if err = c.readValidateDatabaseNameRegexp(c.OpConfig.DatabaseNameRegexp); err != nil { + return err + } + if err = c.initUsers(); err != nil { return err } @@ -337,6 +341,7 @@ func (c *Cluster) Create() error { // that feature explicitly if !(c.databaseAccessDisabled() || c.getNumberOfInstances(&c.Spec) <= 0 || c.Spec.StandbyCluster != nil) { c.logger.Infof("Create roles") + if err = c.createRoles(); err != nil { return fmt.Errorf("could not create users: %v", err) } @@ -345,9 +350,11 @@ func (c *Cluster) Create() error { if err = c.syncDatabases(); err != nil { return fmt.Errorf("could not sync databases: %v", err) } + if err = c.syncPreparedDatabases(); err != nil { return fmt.Errorf("could not sync prepared databases: %v", err) } + c.logger.Infof("databases have been successfully created") } diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 29272eac9..d93edd69c 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -46,6 +46,7 @@ var cl = New( Resources: config.Resources{ DownscalerAnnotations: []string{"downscaler/*"}, }, + DatabaseNameRegexp: "^[a-zA-Z_][a-zA-Z0-9_]*$", }, }, k8sutil.NewMockKubernetesClient(), diff --git a/pkg/cluster/database.go b/pkg/cluster/database.go index 3ec36bb67..1099a124e 100644 --- a/pkg/cluster/database.go +++ b/pkg/cluster/database.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "net" + "regexp" "strings" "text/template" "time" @@ -415,6 +416,24 @@ func (c *Cluster) executeCreateDatabaseSchema(databaseName, schemaName, dbOwner "creating database schema", "create database schema") } +// readValidateDatabaseNameRegexp validates the regex expression. If the validation was +// successful, it sets the global regex object variable for validation. +func (c *Cluster) readValidateDatabaseNameRegexp(expr string) error { + if !(len(expr) > 0) { + return fmt.Errorf("The regex expression for the database name validation is not set") + } + + regex, err := regexp.Compile(expr) + if err != nil { + return fmt.Errorf("The regex expression %q for the database name validation is not correct", expr) + } + + databaseNameRegexp = regex + c.logger.Infof("regexp for database name/schema validation has been initialized") + + return nil +} + func (c *Cluster) execCreateDatabaseSchema(databaseName, schemaName, dbOwner, schemaOwner, statement, doing, operation string) error { if !c.databaseSchemaNameValid(schemaName) { return nil diff --git a/pkg/cluster/database_test.go b/pkg/cluster/database_test.go new file mode 100644 index 000000000..9263da968 --- /dev/null +++ b/pkg/cluster/database_test.go @@ -0,0 +1,31 @@ +package cluster + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadValidateDatabaseNameRegexp(t *testing.T) { + tests := []struct { + name string + expression string + wantErrMsg string + }{ + {"default value", databaseNameRegexp.String(), ""}, + {"null value", "", "validation is not set"}, + {"wrong expression", "12(@3202@@!)#)$$%#$_!@@!_*%_@", "validation is not correct"}, + {"correct expression", "^[a-z0-9]([-_a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-_a-z0-9]*[a-z0-9])?)*$", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := cl.readValidateDatabaseNameRegexp(tt.expression) + if len(tt.wantErrMsg) > 0 { + assert.Containsf(t, err.Error(), tt.wantErrMsg, "expected error containing %q, got %s", tt.wantErrMsg, err) + } else { + assert.Nil(t, err) + } + }) + } +} diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index df1cf6bb8..54171f5a4 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -2,11 +2,10 @@ package config import ( "encoding/json" + "fmt" "strings" "time" - "fmt" - "github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/util/constants" v1 "k8s.io/api/core/v1" @@ -246,6 +245,7 @@ type Config struct { PatroniAPICheckInterval time.Duration `name:"patroni_api_check_interval" default:"1s"` PatroniAPICheckTimeout time.Duration `name:"patroni_api_check_timeout" default:"5s"` EnablePatroniFailsafeMode *bool `name:"enable_patroni_failsafe_mode" default:"false"` + DatabaseNameRegexp string `name:"database_name_regexp" default:"^[a-zA-Z_][a-zA-Z0-9_]*$"` } // MustMarshal marshals the config or panics