reflect code review and additional refactoring
This commit is contained in:
parent
47aa7b1c64
commit
9007475ed8
|
|
@ -536,11 +536,18 @@ Those parameters are grouped under the `tls` top-level key.
|
|||
|
||||
## Change data capture streams
|
||||
|
||||
This sections enables change data capture (CDC) streams e.g. into Zalando’s
|
||||
distributed event broker [Nakadi](https://nakadi.io/). Parameters grouped
|
||||
under the `streams` top-level key will be used by the operator to create
|
||||
custom resources for Zalando's internal CDC operator. Each stream object can
|
||||
have the following properties:
|
||||
This sections enables change data capture (CDC) streams via Postgres'
|
||||
[logical decoding](https://www.postgresql.org/docs/14/logicaldecoding.html)
|
||||
feature and `pgoutput` plugin. While the Postgres operator takes responsibility
|
||||
for providing the setup to publish change events, it relies on external tools
|
||||
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 Zalando’s 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**
|
||||
The application name to which the database and CDC belongs to. For each
|
||||
|
|
|
|||
|
|
@ -15,32 +15,22 @@ import (
|
|||
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")
|
||||
|
||||
var (
|
||||
fes *zalandov1.FabricEventStream
|
||||
err error
|
||||
)
|
||||
|
||||
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)
|
||||
fes := c.generateFabricEventStream(appId)
|
||||
if _, err := c.KubeClient.FabricEventStreams(c.Namespace).Create(context.TODO(), fes, metav1.CreateOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) updateStreams(newEventStreams *zalandov1.FabricEventStream) error {
|
||||
c.setProcessName("updating event streams")
|
||||
|
||||
_, err := c.KubeClient.FabricEventStreams(newEventStreams.Namespace).Update(context.TODO(), newEventStreams, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not update event stream custom resource: %v", err)
|
||||
if _, err := c.KubeClient.FabricEventStreams(newEventStreams.Namespace).Update(context.TODO(), newEventStreams, metav1.UpdateOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -50,7 +40,7 @@ func (c *Cluster) deleteStreams() error {
|
|||
c.setProcessName("deleting event streams")
|
||||
|
||||
// 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) {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -74,9 +64,39 @@ func gatherApplicationIds(streams []acidv1.Stream) []string {
|
|||
return appIds
|
||||
}
|
||||
|
||||
func (c *Cluster) syncPostgresConfig() error {
|
||||
slots := make(map[string]map[string]string)
|
||||
publications := make(map[string]map[string]acidv1.StreamTable)
|
||||
func (c *Cluster) syncPostgresConfig(requiredPatroniConfig acidv1.Patroni) error {
|
||||
errorMsg := "no pods found to update config"
|
||||
|
||||
// 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)
|
||||
alterPublications := make(map[string]string)
|
||||
|
||||
|
|
@ -86,106 +106,45 @@ func (c *Cluster) syncPostgresConfig() error {
|
|||
}
|
||||
}()
|
||||
|
||||
desiredPatroniConfig := c.Spec.Patroni
|
||||
if len(desiredPatroniConfig.Slots) > 0 {
|
||||
slots = desiredPatroniConfig.Slots
|
||||
// check for existing publications
|
||||
if err := c.initDbConnWithName(dbName); err != nil {
|
||||
return fmt.Errorf("could not init database connection")
|
||||
}
|
||||
|
||||
// define extra logical slots for Patroni config
|
||||
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
|
||||
}
|
||||
currentPublications, err := c.getPublications()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get current publications: %v", err)
|
||||
}
|
||||
|
||||
if len(slots) > 0 {
|
||||
desiredPatroniConfig.Slots = slots
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
// if streams are defined wal_level must be switched to logical
|
||||
desiredPgParameters := map[string]string{"wal_level": "logical"}
|
||||
|
||||
// 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 createPublications {
|
||||
if err = c.executeCreatePublication(publicationName, tables); err != nil {
|
||||
return fmt.Errorf("creation of publication %q failed: %v", publicationName, 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"]
|
||||
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)
|
||||
}
|
||||
for publicationName, tables := range alterPublications {
|
||||
if err = c.executeAlterPublication(publicationName, tables); err != nil {
|
||||
return fmt.Errorf("update of publication %q failed: %v", publicationName, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -213,8 +172,8 @@ func (c *Cluster) generateFabricEventStream(appId string) *zalandov1.FabricEvent
|
|||
|
||||
return &zalandov1.FabricEventStream{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: constants.EventStreamSourceCRDKind,
|
||||
APIVersion: "zalando.org/v1",
|
||||
APIVersion: constants.EventStreamCRDApiVersion,
|
||||
Kind: constants.EventStreamCRDKind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s-%s", c.Name, appId),
|
||||
|
|
@ -300,15 +259,65 @@ func (c *Cluster) syncStreams() error {
|
|||
|
||||
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) {
|
||||
c.logger.Debugf("event stream CRD not installed, skipping")
|
||||
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 {
|
||||
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()
|
||||
|
|
@ -331,7 +340,11 @@ func (c *Cluster) createOrUpdateStreams() error {
|
|||
}
|
||||
|
||||
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 {
|
||||
desiredStreams := c.generateFabricEventStream(appId)
|
||||
if !reflect.DeepEqual(effectiveStreams.Spec, desiredStreams.Spec) {
|
||||
|
|
@ -341,6 +354,7 @@ func (c *Cluster) createOrUpdateStreams() error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed updating event stream %s: %v", fesName, err)
|
||||
}
|
||||
c.logger.Infof("event stream %q has been successfully updated", fesName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ var (
|
|||
|
||||
fes = &v1.FabricEventStream{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "FabricEventStream",
|
||||
APIVersion: "zalando.org/v1",
|
||||
APIVersion: constants.EventStreamCRDApiVersion,
|
||||
Kind: constants.EventStreamCRDKind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fesName,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ package constants
|
|||
|
||||
// PostgreSQL specific constants
|
||||
const (
|
||||
EventStreamSourceCRDKind = "FabricEventStream"
|
||||
EventStreamSourceCRDName = "fabriceventstreams.zalando.org"
|
||||
EventStreamCRDApiVersion = "zalando.org/v1"
|
||||
EventStreamCRDKind = "FabricEventStream"
|
||||
EventStreamCRDName = "fabriceventstreams.zalando.org"
|
||||
EventStreamSourcePGType = "PostgresLogicalReplication"
|
||||
EventStreamSourceSlotPrefix = "fes"
|
||||
EventStreamSourcePluginType = "pgoutput"
|
||||
|
|
|
|||
Loading…
Reference in New Issue