reflect code review and additional refactoring

This commit is contained in:
Felix Kunde 2022-02-21 17:01:58 +01:00
parent 47aa7b1c64
commit 9007475ed8
4 changed files with 150 additions and 128 deletions

View File

@ -536,11 +536,18 @@ Those parameters are grouped under the `tls` top-level key.
## Change data capture streams ## Change data capture streams
This sections enables change data capture (CDC) streams e.g. into Zalandos This sections enables change data capture (CDC) streams via Postgres'
distributed event broker [Nakadi](https://nakadi.io/). Parameters grouped [logical decoding](https://www.postgresql.org/docs/14/logicaldecoding.html)
under the `streams` top-level key will be used by the operator to create feature and `pgoutput` plugin. While the Postgres operator takes responsibility
custom resources for Zalando's internal CDC operator. Each stream object can for providing the setup to publish change events, it relies on external tools
have the following properties: to consume them. At Zalando, we are using a workflow based on
[Debezium Connector](https://debezium.io/documentation/reference/stable/connectors/postgresql.html)
which can feed streams into Zalandos distributed event broker [Nakadi](https://nakadi.io/)
among others.
The Postgres Operator creates custom resources for Zalando's internal CDC
operator which will be used to set up the consumer part. Each stream object
can have the following properties:
* **applicationId** * **applicationId**
The application name to which the database and CDC belongs to. For each The application name to which the database and CDC belongs to. For each

View File

@ -15,32 +15,22 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
func (c *Cluster) createStreams(appId string) { func (c *Cluster) createStreams(appId string) error {
c.setProcessName("creating streams") c.setProcessName("creating streams")
var ( fes := c.generateFabricEventStream(appId)
fes *zalandov1.FabricEventStream if _, err := c.KubeClient.FabricEventStreams(c.Namespace).Create(context.TODO(), fes, metav1.CreateOptions{}); err != nil {
err error return err
)
msg := "could not create event stream custom resource with applicationId %s: %v"
fes = c.generateFabricEventStream(appId)
if err != nil {
c.logger.Warningf(msg, appId, err)
}
_, err = c.KubeClient.FabricEventStreams(c.Namespace).Create(context.TODO(), fes, metav1.CreateOptions{})
if err != nil {
c.logger.Warningf(msg, appId, err)
} }
return nil
} }
func (c *Cluster) updateStreams(newEventStreams *zalandov1.FabricEventStream) error { func (c *Cluster) updateStreams(newEventStreams *zalandov1.FabricEventStream) error {
c.setProcessName("updating event streams") c.setProcessName("updating event streams")
_, err := c.KubeClient.FabricEventStreams(newEventStreams.Namespace).Update(context.TODO(), newEventStreams, metav1.UpdateOptions{}) if _, err := c.KubeClient.FabricEventStreams(newEventStreams.Namespace).Update(context.TODO(), newEventStreams, metav1.UpdateOptions{}); err != nil {
if err != nil { return err
return fmt.Errorf("could not update event stream custom resource: %v", err)
} }
return nil return nil
@ -50,7 +40,7 @@ func (c *Cluster) deleteStreams() error {
c.setProcessName("deleting event streams") c.setProcessName("deleting event streams")
// check if stream CRD is installed before trying a delete // check if stream CRD is installed before trying a delete
_, err := c.KubeClient.CustomResourceDefinitions().Get(context.TODO(), constants.EventStreamSourceCRDName, metav1.GetOptions{}) _, err := c.KubeClient.CustomResourceDefinitions().Get(context.TODO(), constants.EventStreamCRDName, metav1.GetOptions{})
if k8sutil.ResourceNotFound(err) { if k8sutil.ResourceNotFound(err) {
return nil return nil
} }
@ -74,9 +64,39 @@ func gatherApplicationIds(streams []acidv1.Stream) []string {
return appIds return appIds
} }
func (c *Cluster) syncPostgresConfig() error { func (c *Cluster) syncPostgresConfig(requiredPatroniConfig acidv1.Patroni) error {
slots := make(map[string]map[string]string) errorMsg := "no pods found to update config"
publications := make(map[string]map[string]acidv1.StreamTable)
// if streams are defined wal_level must be switched to logical
requiredPgParameters := map[string]string{"wal_level": "logical"}
// apply config changes in pods
pods, err := c.listPods()
if err != nil {
errorMsg = fmt.Sprintf("could not list pods of the statefulset: %v", err)
}
for i, pod := range pods {
podName := util.NameFromMeta(pods[i].ObjectMeta)
effectivePatroniConfig, effectivePgParameters, err := c.patroni.GetConfig(&pod)
if err != nil {
errorMsg = fmt.Sprintf("could not get Postgres config from pod %s: %v", podName, err)
continue
}
_, err = c.checkAndSetGlobalPostgreSQLConfiguration(&pod, effectivePatroniConfig, requiredPatroniConfig, effectivePgParameters, requiredPgParameters)
if err != nil {
errorMsg = fmt.Sprintf("could not set PostgreSQL configuration options for pod %s: %v", podName, err)
continue
}
// Patroni's config endpoint is just a "proxy" to DCS. It is enough to patch it only once and it doesn't matter which pod is used
return nil
}
return fmt.Errorf(errorMsg)
}
func (c *Cluster) syncPublication(publication, dbName string, tables map[string]acidv1.StreamTable) error {
createPublications := make(map[string]string) createPublications := make(map[string]string)
alterPublications := make(map[string]string) alterPublications := make(map[string]string)
@ -86,106 +106,45 @@ func (c *Cluster) syncPostgresConfig() error {
} }
}() }()
desiredPatroniConfig := c.Spec.Patroni // check for existing publications
if len(desiredPatroniConfig.Slots) > 0 { if err := c.initDbConnWithName(dbName); err != nil {
slots = desiredPatroniConfig.Slots return fmt.Errorf("could not init database connection")
} }
// define extra logical slots for Patroni config currentPublications, err := c.getPublications()
for _, stream := range c.Spec.Streams { if err != nil {
slot := map[string]string{ return fmt.Errorf("could not get current publications: %v", err)
"database": stream.Database,
"plugin": constants.EventStreamSourcePluginType,
"type": "logical",
}
slotName := getSlotName(stream.Database, stream.ApplicationId)
if _, exists := slots[slotName]; !exists {
slots[slotName] = slot
publications[slotName] = stream.Tables
} else {
streamTables := publications[slotName]
for tableName, table := range stream.Tables {
if _, exists := streamTables[tableName]; !exists {
streamTables[tableName] = table
}
}
publications[slotName] = streamTables
}
} }
if len(slots) > 0 { tableNames := make([]string, len(tables))
desiredPatroniConfig.Slots = slots i := 0
} else { for t := range tables {
tableName, schemaName := getTableSchema(t)
tableNames[i] = fmt.Sprintf("%s.%s", schemaName, tableName)
i++
}
sort.Strings(tableNames)
tableList := strings.Join(tableNames, ", ")
currentTables, exists := currentPublications[publication]
if !exists {
createPublications[publication] = tableList
} else if currentTables != tableList {
alterPublications[publication] = tableList
}
if len(createPublications)+len(alterPublications) == 0 {
return nil return nil
} }
// if streams are defined wal_level must be switched to logical for publicationName, tables := range createPublications {
desiredPgParameters := map[string]string{"wal_level": "logical"} if err = c.executeCreatePublication(publicationName, tables); err != nil {
return fmt.Errorf("creation of publication %q failed: %v", publicationName, err)
// apply config changes in pods
pods, err := c.listPods()
if err != nil || len(pods) == 0 {
c.logger.Warningf("could not list pods of the statefulset: %v", err)
}
for i, pod := range pods {
podName := util.NameFromMeta(pods[i].ObjectMeta)
effectivePatroniConfig, effectivePgParameters, err := c.patroni.GetConfig(&pod)
if err != nil {
c.logger.Warningf("could not get Postgres config from pod %s: %v", podName, err)
continue
}
_, err = c.checkAndSetGlobalPostgreSQLConfiguration(&pod, effectivePatroniConfig, desiredPatroniConfig, effectivePgParameters, desiredPgParameters)
if err != nil {
c.logger.Warningf("could not set PostgreSQL configuration options for pod %s: %v", podName, err)
continue
} }
} }
for publicationName, tables := range alterPublications {
// next, create publications to each created slot if err = c.executeAlterPublication(publicationName, tables); err != nil {
c.logger.Debug("syncing database publications") return fmt.Errorf("update of publication %q failed: %v", publicationName, err)
for publication, tables := range publications {
// but first check for existing publications
dbName := slots[publication]["database"]
if err := c.initDbConnWithName(dbName); err != nil {
return fmt.Errorf("could not init database connection")
}
currentPublications, err := c.getPublications()
if err != nil {
return fmt.Errorf("could not get current publications: %v", err)
}
tableNames := make([]string, len(tables))
i := 0
for t := range tables {
tableName, schemaName := getTableSchema(t)
tableNames[i] = fmt.Sprintf("%s.%s", schemaName, tableName)
i++
}
sort.Strings(tableNames)
tableList := strings.Join(tableNames, ", ")
currentTables, exists := currentPublications[publication]
if !exists {
createPublications[publication] = tableList
} else if currentTables != tableList {
alterPublications[publication] = tableList
}
if len(createPublications)+len(alterPublications) == 0 {
return nil
}
for publicationName, tables := range createPublications {
if err = c.executeCreatePublication(publicationName, tables); err != nil {
c.logger.Warningf("%v", err)
}
}
for publicationName, tables := range alterPublications {
if err = c.executeAlterPublication(publicationName, tables); err != nil {
c.logger.Warningf("%v", err)
}
} }
} }
@ -213,8 +172,8 @@ func (c *Cluster) generateFabricEventStream(appId string) *zalandov1.FabricEvent
return &zalandov1.FabricEventStream{ return &zalandov1.FabricEventStream{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: constants.EventStreamSourceCRDKind, APIVersion: constants.EventStreamCRDApiVersion,
APIVersion: "zalando.org/v1", Kind: constants.EventStreamCRDKind,
}, },
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s", c.Name, appId), Name: fmt.Sprintf("%s-%s", c.Name, appId),
@ -300,15 +259,65 @@ func (c *Cluster) syncStreams() error {
c.setProcessName("syncing streams") c.setProcessName("syncing streams")
_, err := c.KubeClient.CustomResourceDefinitions().Get(context.TODO(), constants.EventStreamSourceCRDName, metav1.GetOptions{}) _, err := c.KubeClient.CustomResourceDefinitions().Get(context.TODO(), constants.EventStreamCRDName, metav1.GetOptions{})
if k8sutil.ResourceNotFound(err) { if k8sutil.ResourceNotFound(err) {
c.logger.Debugf("event stream CRD not installed, skipping") c.logger.Debugf("event stream CRD not installed, skipping")
return nil return nil
} }
err = c.syncPostgresConfig() slots := make(map[string]map[string]string)
publications := make(map[string]map[string]acidv1.StreamTable)
requiredPatroniConfig := c.Spec.Patroni
if len(requiredPatroniConfig.Slots) > 0 {
slots = requiredPatroniConfig.Slots
}
// gather list of required slots and publications
for _, stream := range c.Spec.Streams {
slot := map[string]string{
"database": stream.Database,
"plugin": constants.EventStreamSourcePluginType,
"type": "logical",
}
slotName := getSlotName(stream.Database, stream.ApplicationId)
if _, exists := slots[slotName]; !exists {
slots[slotName] = slot
publications[slotName] = stream.Tables
} else {
streamTables := publications[slotName]
for tableName, table := range stream.Tables {
if _, exists := streamTables[tableName]; !exists {
streamTables[tableName] = table
}
}
publications[slotName] = streamTables
}
}
// no slots = no streams defined
if len(slots) > 0 {
requiredPatroniConfig.Slots = slots
} else {
return nil
}
// add extra logical slots to Patroni config
c.logger.Debug("syncing Postgres config for logical decoding")
err = c.syncPostgresConfig(requiredPatroniConfig)
if err != nil { if err != nil {
return fmt.Errorf("could not update Postgres config for event streaming: %v", err) return fmt.Errorf("failed to snyc Postgres config for event streaming: %v", err)
}
// next, create publications to each created slot
c.logger.Debug("syncing database publications")
for publication, tables := range publications {
// but first check for existing publications
dbName := slots[publication]["database"]
err = c.syncPublication(publication, dbName, tables)
if err != nil {
c.logger.Warningf("could not sync publication %q in database %d: %v", publication, dbName, err)
}
} }
err = c.createOrUpdateStreams() err = c.createOrUpdateStreams()
@ -331,7 +340,11 @@ func (c *Cluster) createOrUpdateStreams() error {
} }
c.logger.Infof("event streams do not exist, create it") c.logger.Infof("event streams do not exist, create it")
c.createStreams(appId) err = c.createStreams(appId)
if err != nil {
return fmt.Errorf("failed creating event stream %s: %v", fesName, err)
}
c.logger.Infof("event stream %q has been successfully created", fesName)
} else { } else {
desiredStreams := c.generateFabricEventStream(appId) desiredStreams := c.generateFabricEventStream(appId)
if !reflect.DeepEqual(effectiveStreams.Spec, desiredStreams.Spec) { if !reflect.DeepEqual(effectiveStreams.Spec, desiredStreams.Spec) {
@ -341,6 +354,7 @@ func (c *Cluster) createOrUpdateStreams() error {
if err != nil { if err != nil {
return fmt.Errorf("failed updating event stream %s: %v", fesName, err) return fmt.Errorf("failed updating event stream %s: %v", fesName, err)
} }
c.logger.Infof("event stream %q has been successfully updated", fesName)
} }
} }
} }

View File

@ -82,8 +82,8 @@ var (
fes = &v1.FabricEventStream{ fes = &v1.FabricEventStream{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: "FabricEventStream", APIVersion: constants.EventStreamCRDApiVersion,
APIVersion: "zalando.org/v1", Kind: constants.EventStreamCRDKind,
}, },
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: fesName, Name: fesName,

View File

@ -2,8 +2,9 @@ package constants
// PostgreSQL specific constants // PostgreSQL specific constants
const ( const (
EventStreamSourceCRDKind = "FabricEventStream" EventStreamCRDApiVersion = "zalando.org/v1"
EventStreamSourceCRDName = "fabriceventstreams.zalando.org" EventStreamCRDKind = "FabricEventStream"
EventStreamCRDName = "fabriceventstreams.zalando.org"
EventStreamSourcePGType = "PostgresLogicalReplication" EventStreamSourcePGType = "PostgresLogicalReplication"
EventStreamSourceSlotPrefix = "fes" EventStreamSourceSlotPrefix = "fes"
EventStreamSourcePluginType = "pgoutput" EventStreamSourcePluginType = "pgoutput"