introduce applicationId for separate stream CRDs
This commit is contained in:
parent
96a2da1fca
commit
74ee530a6c
|
|
@ -475,9 +475,12 @@ spec:
|
|||
items:
|
||||
type: object
|
||||
required:
|
||||
- applicationId
|
||||
- database
|
||||
- tables
|
||||
properties:
|
||||
applicationId:
|
||||
type: string
|
||||
batchSize:
|
||||
type: integer
|
||||
database:
|
||||
|
|
|
|||
|
|
@ -522,9 +522,17 @@ Those parameters are grouped under the `tls` top-level key.
|
|||
|
||||
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 a
|
||||
CRD for Zalando's internal CDC operator named like the Postgres cluster.
|
||||
Each stream object can have the following properties:
|
||||
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:
|
||||
|
||||
* **applicationId**
|
||||
The application name to which the database and CDC belongs to. For each
|
||||
set of streams with a distinct `applicationId` a separate stream CR as well
|
||||
as a separate logical replication slot will be created. This means there can
|
||||
different streams in the same database and streams with the same
|
||||
`applicationId` are bundled in one stream CR. The stream CR will be called
|
||||
like the Postgres cluster plus "-<applicationId>" suffix. Required.
|
||||
|
||||
* **database**
|
||||
Name of the database from where events will be published via Postgres'
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ kind: postgresql
|
|||
metadata:
|
||||
name: acid-test-cluster
|
||||
# labels:
|
||||
# application: test-app
|
||||
# environment: demo
|
||||
# annotations:
|
||||
# "acid.zalan.do/controller": "second-operator"
|
||||
|
|
@ -198,15 +199,16 @@ spec:
|
|||
|
||||
# Enables change data capture streams for defined database tables
|
||||
# streams:
|
||||
# - database: foo
|
||||
# - applicationId: test-app
|
||||
# database: foo
|
||||
# tables:
|
||||
# data.ta:
|
||||
# data.tab_a:
|
||||
# eventType: event_type_a
|
||||
# data.tb:
|
||||
# data.tab_b:
|
||||
# eventType: event_type_b
|
||||
# idColumn: tb_id
|
||||
# payloadColumn: tb_payload
|
||||
# # Optional. Filter ignores events before a certain txnId and lsn. Can be used to skip bad events
|
||||
# filter:
|
||||
# data.ta: "[?(@.source.txId > 500 && @.source.lsn > 123456)]"
|
||||
# data.tab_a: "[?(@.source.txId > 500 && @.source.lsn > 123456)]"
|
||||
# batchSize: 1000
|
||||
|
|
|
|||
|
|
@ -473,9 +473,12 @@ spec:
|
|||
items:
|
||||
type: object
|
||||
required:
|
||||
- applicationId
|
||||
- database
|
||||
- tables
|
||||
properties:
|
||||
applicationId:
|
||||
type: string
|
||||
batchSize:
|
||||
type: integer
|
||||
database:
|
||||
|
|
|
|||
|
|
@ -664,8 +664,11 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
|
|||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Required: []string{"database", "tables"},
|
||||
Required: []string{"applicationId", "database", "tables"},
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"applicationId": {
|
||||
Type: "string",
|
||||
},
|
||||
"batchSize": {
|
||||
Type: "integer",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -230,10 +230,11 @@ type ConnectionPooler struct {
|
|||
}
|
||||
|
||||
type Stream struct {
|
||||
Database string `json:"database"`
|
||||
Tables map[string]StreamTable `json:"tables"`
|
||||
Filter map[string]string `json:"filter,omitempty"`
|
||||
BatchSize uint32 `json:"batchSize,omitempty"`
|
||||
ApplicationId string `json:"applicationId"`
|
||||
Database string `json:"database"`
|
||||
Tables map[string]StreamTable `json:"tables"`
|
||||
Filter map[string]string `json:"filter,omitempty"`
|
||||
BatchSize uint32 `json:"batchSize,omitempty"`
|
||||
}
|
||||
|
||||
type StreamTable struct {
|
||||
|
|
|
|||
|
|
@ -14,16 +14,24 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func (c *Cluster) createStreams() error {
|
||||
func (c *Cluster) createStreams(appId string) {
|
||||
c.setProcessName("creating streams")
|
||||
|
||||
fes := c.generateFabricEventStream()
|
||||
_, err := c.KubeClient.FabricEventStreams(c.Namespace).Create(context.TODO(), fes, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create event stream custom resource: %v", err)
|
||||
}
|
||||
var (
|
||||
fes *zalandov1alpha1.FabricEventStream
|
||||
err error
|
||||
)
|
||||
|
||||
return nil
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cluster) updateStreams(newEventStreams *zalandov1alpha1.FabricEventStream) error {
|
||||
|
|
@ -54,21 +62,34 @@ func (c *Cluster) deleteStreams() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func gatherApplicationIds(streams []acidv1.Stream) []string {
|
||||
appIds := make([]string, 0)
|
||||
for _, stream := range streams {
|
||||
if !util.SliceContains(appIds, stream.ApplicationId) {
|
||||
appIds = append(appIds, stream.ApplicationId)
|
||||
}
|
||||
}
|
||||
|
||||
return appIds
|
||||
}
|
||||
|
||||
func (c *Cluster) syncPostgresConfig() error {
|
||||
|
||||
slots := make(map[string]map[string]string)
|
||||
desiredPatroniConfig := c.Spec.Patroni
|
||||
slots := desiredPatroniConfig.Slots
|
||||
if len(desiredPatroniConfig.Slots) > 0 {
|
||||
slots = desiredPatroniConfig.Slots
|
||||
}
|
||||
|
||||
for _, stream := range c.Spec.Streams {
|
||||
slotName := c.getLogicalReplicationSlot(stream.Database)
|
||||
|
||||
if slotName == "" {
|
||||
slot := map[string]string{
|
||||
"database": stream.Database,
|
||||
"plugin": "wal2json",
|
||||
"type": "logical",
|
||||
}
|
||||
slots[constants.EventStreamSourceSlotPrefix+"_"+stream.Database] = slot
|
||||
slot := map[string]string{
|
||||
"database": stream.Database,
|
||||
"plugin": "wal2json",
|
||||
"type": "logical",
|
||||
}
|
||||
slotName := constants.EventStreamSourceSlotPrefix + "_" + stream.Database + "_" + stream.ApplicationId
|
||||
if _, exists := slots[slotName]; !exists {
|
||||
slots[slotName] = slot
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,16 +128,13 @@ func (c *Cluster) syncPostgresConfig() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) generateFabricEventStream() *zalandov1alpha1.FabricEventStream {
|
||||
var applicationId string
|
||||
func (c *Cluster) generateFabricEventStream(appId string) *zalandov1alpha1.FabricEventStream {
|
||||
eventStreams := make([]zalandov1alpha1.EventStream, 0)
|
||||
|
||||
// take application label from manifest
|
||||
if spec, err := c.GetSpec(); err == nil {
|
||||
applicationId = spec.ObjectMeta.Labels["application"]
|
||||
}
|
||||
|
||||
for _, stream := range c.Spec.Streams {
|
||||
if stream.ApplicationId != appId {
|
||||
continue
|
||||
}
|
||||
for tableName, table := range stream.Tables {
|
||||
streamSource := c.getEventStreamSource(stream, tableName, table.IdColumn)
|
||||
streamFlow := getEventStreamFlow(stream, table.PayloadColumn)
|
||||
|
|
@ -135,14 +153,14 @@ func (c *Cluster) generateFabricEventStream() *zalandov1alpha1.FabricEventStream
|
|||
APIVersion: "zalando.org/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: c.Name,
|
||||
Name: c.Name + "-" + appId,
|
||||
Namespace: c.Namespace,
|
||||
Annotations: c.AnnotationsToPropagate(c.annotationsSet(nil)),
|
||||
// make cluster StatefulSet the owner (like with connection pooler objects)
|
||||
OwnerReferences: c.ownerReferences(),
|
||||
},
|
||||
Spec: zalandov1alpha1.FabricEventStreamSpec{
|
||||
ApplicationId: applicationId,
|
||||
ApplicationId: appId,
|
||||
EventStreams: eventStreams,
|
||||
},
|
||||
}
|
||||
|
|
@ -156,7 +174,10 @@ func (c *Cluster) getEventStreamSource(stream acidv1.Stream, tableName, idColumn
|
|||
Schema: schema,
|
||||
EventStreamTable: getOutboxTable(table, idColumn),
|
||||
Filter: streamFilter,
|
||||
Connection: c.getStreamConnection(stream.Database, constants.EventStreamSourceSlotPrefix+constants.UserRoleNameSuffix),
|
||||
Connection: c.getStreamConnection(
|
||||
stream.Database,
|
||||
constants.EventStreamSourceSlotPrefix+constants.UserRoleNameSuffix,
|
||||
stream.ApplicationId),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,10 +214,10 @@ func getOutboxTable(tableName, idColumn string) zalandov1alpha1.EventStreamTable
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Cluster) getStreamConnection(database, user string) zalandov1alpha1.Connection {
|
||||
func (c *Cluster) getStreamConnection(database, user, appId string) zalandov1alpha1.Connection {
|
||||
return zalandov1alpha1.Connection{
|
||||
Url: fmt.Sprintf("jdbc:postgresql://%s.%s/%s?user=%s&ssl=true&sslmode=require", c.Name, c.Namespace, database, user),
|
||||
SlotName: c.getLogicalReplicationSlot(database),
|
||||
SlotName: constants.EventStreamSourceSlotPrefix + "_" + database + "_" + appId,
|
||||
DBAuth: zalandov1alpha1.DBAuth{
|
||||
Type: constants.EventStreamSourceAuthType,
|
||||
Name: c.credentialSecretNameForCluster(user, c.Name),
|
||||
|
|
@ -206,16 +227,6 @@ func (c *Cluster) getStreamConnection(database, user string) zalandov1alpha1.Con
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Cluster) getLogicalReplicationSlot(database string) string {
|
||||
for slotName, slot := range c.Spec.Patroni.Slots {
|
||||
if slot["type"] == "logical" && slot["database"] == database && slot["plugin"] == "wal2json" {
|
||||
return slotName
|
||||
}
|
||||
}
|
||||
|
||||
return constants.EventStreamSourceSlotPrefix + "_" + database
|
||||
}
|
||||
|
||||
func (c *Cluster) syncStreams() error {
|
||||
|
||||
_, err := c.KubeClient.CustomResourceDefinitions().Get(context.TODO(), constants.EventStreamSourceCRDName, metav1.GetOptions{})
|
||||
|
|
@ -241,25 +252,26 @@ func (c *Cluster) createOrUpdateStreams() error {
|
|||
return fmt.Errorf("could not update Postgres config for event streaming: %v", err)
|
||||
}
|
||||
|
||||
effectiveStreams, err := c.KubeClient.FabricEventStreams(c.Namespace).Get(context.TODO(), c.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if !k8sutil.ResourceNotFound(err) {
|
||||
return fmt.Errorf("error during reading of event streams: %v", err)
|
||||
}
|
||||
|
||||
c.logger.Infof("event streams do not exist, create it")
|
||||
err := c.createStreams()
|
||||
appIds := gatherApplicationIds(c.Spec.Streams)
|
||||
for _, appId := range appIds {
|
||||
fesName := c.Name + "-" + appId
|
||||
effectiveStreams, err := c.KubeClient.FabricEventStreams(c.Namespace).Get(context.TODO(), fesName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("event streams creation failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
desiredStreams := c.generateFabricEventStream()
|
||||
if !reflect.DeepEqual(effectiveStreams.Spec, desiredStreams.Spec) {
|
||||
c.logger.Debug("updating event streams")
|
||||
desiredStreams.ObjectMeta.ResourceVersion = effectiveStreams.ObjectMeta.ResourceVersion
|
||||
err = c.updateStreams(desiredStreams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("event streams update failed: %v", err)
|
||||
if !k8sutil.ResourceNotFound(err) {
|
||||
return fmt.Errorf("failed reading event stream %s: %v", fesName, err)
|
||||
}
|
||||
|
||||
c.logger.Infof("event streams do not exist, create it")
|
||||
c.createStreams(appId)
|
||||
} else {
|
||||
desiredStreams := c.generateFabricEventStream(appId)
|
||||
if !reflect.DeepEqual(effectiveStreams.Spec, desiredStreams.Spec) {
|
||||
c.logger.Debug("updating event streams")
|
||||
desiredStreams.ObjectMeta.ResourceVersion = effectiveStreams.ObjectMeta.ResourceVersion
|
||||
err = c.updateStreams(desiredStreams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed updating event stream %s: %v", fesName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@ func newFakeK8sStreamClient() (k8sutil.KubernetesClient, *fake.Clientset) {
|
|||
var (
|
||||
clusterName string = "acid-test-cluster"
|
||||
namespace string = "default"
|
||||
appId string = "test-app"
|
||||
dbName string = "foo"
|
||||
fesUser string = constants.EventStreamSourceSlotPrefix + constants.UserRoleNameSuffix
|
||||
fesName string = clusterName + "-" + appId
|
||||
|
||||
pg = acidv1.Postgresql{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
|
|
@ -47,15 +50,15 @@ var (
|
|||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterName,
|
||||
Namespace: namespace,
|
||||
Labels: map[string]string{"application": "test"},
|
||||
},
|
||||
Spec: acidv1.PostgresSpec{
|
||||
Databases: map[string]string{
|
||||
"foo": "foo_user",
|
||||
dbName: dbName + constants.UserRoleNameSuffix,
|
||||
},
|
||||
Streams: []acidv1.Stream{
|
||||
{
|
||||
Database: "foo",
|
||||
ApplicationId: appId,
|
||||
Database: "foo",
|
||||
Tables: map[string]acidv1.StreamTable{
|
||||
"data.bar": acidv1.StreamTable{
|
||||
EventType: "stream_type_a",
|
||||
|
|
@ -69,9 +72,6 @@ var (
|
|||
BatchSize: uint32(100),
|
||||
},
|
||||
},
|
||||
Users: map[string]acidv1.UserFlags{
|
||||
"foo_user": []string{"replication"},
|
||||
},
|
||||
Volume: acidv1.Volume{
|
||||
Size: "1Gi",
|
||||
},
|
||||
|
|
@ -84,7 +84,7 @@ var (
|
|||
APIVersion: "zalando.org/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterName,
|
||||
Name: fesName,
|
||||
Namespace: namespace,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
metav1.OwnerReference{
|
||||
|
|
@ -96,7 +96,7 @@ var (
|
|||
},
|
||||
},
|
||||
Spec: v1alpha1.FabricEventStreamSpec{
|
||||
ApplicationId: "test",
|
||||
ApplicationId: appId,
|
||||
EventStreams: []v1alpha1.EventStream{
|
||||
{
|
||||
EventStreamFlow: v1alpha1.EventStreamFlow{
|
||||
|
|
@ -118,7 +118,7 @@ var (
|
|||
UserKey: "username",
|
||||
},
|
||||
Url: fmt.Sprintf("jdbc:postgresql://%s.%s/foo?user=%s&ssl=true&sslmode=require", clusterName, namespace, fesUser),
|
||||
SlotName: "fes_foo",
|
||||
SlotName: fmt.Sprintf("%s_%s_%s", constants.EventStreamSourceSlotPrefix, dbName, appId),
|
||||
},
|
||||
Schema: "data",
|
||||
EventStreamTable: v1alpha1.EventStreamTable{
|
||||
|
|
@ -164,13 +164,13 @@ func TestGenerateFabricEventStream(t *testing.T) {
|
|||
err = cluster.createOrUpdateStreams()
|
||||
assert.NoError(t, err)
|
||||
|
||||
result := cluster.generateFabricEventStream()
|
||||
result := cluster.generateFabricEventStream(appId)
|
||||
|
||||
if !reflect.DeepEqual(result, fes) {
|
||||
t.Errorf("Malformed FabricEventStream, expected %#v, got %#v", fes, result)
|
||||
}
|
||||
|
||||
streamCRD, err := cluster.KubeClient.FabricEventStreams(namespace).Get(context.TODO(), cluster.Name, metav1.GetOptions{})
|
||||
streamCRD, err := cluster.KubeClient.FabricEventStreams(namespace).Get(context.TODO(), fesName, metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !reflect.DeepEqual(streamCRD, fes) {
|
||||
|
|
@ -206,7 +206,8 @@ func TestUpdateFabricEventStream(t *testing.T) {
|
|||
var pgSpec acidv1.PostgresSpec
|
||||
pgSpec.Streams = []acidv1.Stream{
|
||||
{
|
||||
Database: "foo",
|
||||
ApplicationId: appId,
|
||||
Database: dbName,
|
||||
Tables: map[string]acidv1.StreamTable{
|
||||
"data.bar": acidv1.StreamTable{
|
||||
EventType: "stream_type_b",
|
||||
|
|
@ -230,10 +231,10 @@ func TestUpdateFabricEventStream(t *testing.T) {
|
|||
err = cluster.createOrUpdateStreams()
|
||||
assert.NoError(t, err)
|
||||
|
||||
streamCRD, err := cluster.KubeClient.FabricEventStreams(namespace).Get(context.TODO(), cluster.Name, metav1.GetOptions{})
|
||||
streamCRD, err := cluster.KubeClient.FabricEventStreams(namespace).Get(context.TODO(), fesName, metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
result := cluster.generateFabricEventStream()
|
||||
result := cluster.generateFabricEventStream(appId)
|
||||
if !reflect.DeepEqual(result, streamCRD) {
|
||||
t.Errorf("Malformed FabricEventStream, expected %#v, got %#v", streamCRD, result)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue