From 5ef5ad4ce7bae14e5cf72321d7c89e5bdfb79ecd Mon Sep 17 00:00:00 2001 From: Murad Biashimov Date: Fri, 26 Jul 2024 21:02:30 +0200 Subject: [PATCH] fix(kafkaschema): poll resource availability --- CHANGELOG.md | 2 + api/v1alpha1/kafkaschema_types.go | 7 +- .../templates/aiven.io_kafkaschemas.yaml | 7 ++ config/crd/bases/aiven.io_kafkaschemas.yaml | 7 ++ controllers/kafkaschema_controller.go | 95 +++++++++++++------ docs/docs/api-reference/kafkaschema.md | 1 + go.mod | 3 +- go.sum | 6 +- tests/kafkaschema_test.go | 5 +- tests/session.go | 8 -- 10 files changed, 99 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b81c4ce5..3ed77c02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Fix `KafkaTopic`: fails to create a topic with the replication factor set more than running Kafka nodes - Fix `ServiceIntegration`: sends empty source and destination projects +- Fix `KafkaSchema`: poll resource availability +- Add `KafkaSchema` field `schemaType`, type `string`: Schema type - Add `Kafka` field `userConfig.follower_fetching`, type `object`: Enable follower fetching - Change `Kafka` field `userConfig.kafka.sasl_oauthbearer_sub_claim_name`: pattern ~~`^[^\r\n]*$`~~ → `^[^\r\n]*\S[^\r\n]*$` diff --git a/api/v1alpha1/kafkaschema_types.go b/api/v1alpha1/kafkaschema_types.go index aa958d45..3e16972d 100644 --- a/api/v1alpha1/kafkaschema_types.go +++ b/api/v1alpha1/kafkaschema_types.go @@ -3,6 +3,7 @@ package v1alpha1 import ( + "github.com/aiven/go-client-codegen/handler/kafkaschemaregistry" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -17,9 +18,13 @@ type KafkaSchemaSpec struct { // Kafka Schema configuration should be a valid Avro Schema JSON format Schema string `json:"schema"` + // +kubebuilder:validation:Enum=AVRO;JSON;PROTOBUF + // Schema type + SchemaType kafkaschemaregistry.SchemaType `json:"schemaType,omitempty"` + // +kubebuilder:validation:Enum=BACKWARD;BACKWARD_TRANSITIVE;FORWARD;FORWARD_TRANSITIVE;FULL;FULL_TRANSITIVE;NONE // Kafka Schemas compatibility level - CompatibilityLevel string `json:"compatibilityLevel,omitempty"` + CompatibilityLevel kafkaschemaregistry.CompatibilityType `json:"compatibilityLevel,omitempty"` } // KafkaSchemaStatus defines the observed state of KafkaSchema diff --git a/charts/aiven-operator-crds/templates/aiven.io_kafkaschemas.yaml b/charts/aiven-operator-crds/templates/aiven.io_kafkaschemas.yaml index d227a408..5e34946f 100644 --- a/charts/aiven-operator-crds/templates/aiven.io_kafkaschemas.yaml +++ b/charts/aiven-operator-crds/templates/aiven.io_kafkaschemas.yaml @@ -92,6 +92,13 @@ spec: Kafka Schema configuration should be a valid Avro Schema JSON format type: string + schemaType: + description: Schema type + enum: + - AVRO + - JSON + - PROTOBUF + type: string serviceName: description: Specifies the name of the service that this resource diff --git a/config/crd/bases/aiven.io_kafkaschemas.yaml b/config/crd/bases/aiven.io_kafkaschemas.yaml index d227a408..5e34946f 100644 --- a/config/crd/bases/aiven.io_kafkaschemas.yaml +++ b/config/crd/bases/aiven.io_kafkaschemas.yaml @@ -92,6 +92,13 @@ spec: Kafka Schema configuration should be a valid Avro Schema JSON format type: string + schemaType: + description: Schema type + enum: + - AVRO + - JSON + - PROTOBUF + type: string serviceName: description: Specifies the name of the service that this resource diff --git a/controllers/kafkaschema_controller.go b/controllers/kafkaschema_controller.go index d103a2d8..3fcf3666 100644 --- a/controllers/kafkaschema_controller.go +++ b/controllers/kafkaschema_controller.go @@ -6,9 +6,12 @@ import ( "context" "fmt" "strconv" + "time" "github.com/aiven/aiven-go-client/v2" avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/kafkaschemaregistry" + "github.com/avast/retry-go" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -43,20 +46,32 @@ func (r *KafkaSchemaReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (h KafkaSchemaHandler) createOrUpdate(ctx context.Context, avn *aiven.Client, avnGen avngen.Client, obj client.Object, refs []client.Object) error { +func (h KafkaSchemaHandler) createOrUpdate(ctx context.Context, avn *aiven.Client, avnGen avngen.Client, obj client.Object, _ []client.Object) error { schema, err := h.convert(obj) if err != nil { return err } - // createOrUpdate Kafka Schema Subject - _, err = avn.KafkaSubjectSchemas.Add( + // Must poll kafka until the registry ready. + // The client retries errors under the hood. + _, err = avnGen.ServiceSchemaRegistrySubjectVersionsGet( ctx, schema.Spec.Project, schema.Spec.ServiceName, schema.Spec.SubjectName, - aiven.KafkaSchemaSubject{ - Schema: schema.Spec.Schema, + ) + if err != nil && !isNotFound(err) { + return err + } + + schemaID, err := avnGen.ServiceSchemaRegistrySubjectVersionPost( + ctx, + schema.Spec.Project, + schema.Spec.ServiceName, + schema.Spec.SubjectName, + &kafkaschemaregistry.ServiceSchemaRegistrySubjectVersionPostIn{ + Schema: schema.Spec.Schema, + SchemaType: schema.Spec.SchemaType, }, ) if err != nil { @@ -65,26 +80,52 @@ func (h KafkaSchemaHandler) createOrUpdate(ctx context.Context, avn *aiven.Clien // set compatibility level if defined for a newly created Kafka Schema Subject if schema.Spec.CompatibilityLevel != "" { - _, err := avn.KafkaSubjectSchemas.UpdateConfiguration( + _, err := avnGen.ServiceSchemaRegistrySubjectConfigPut( ctx, schema.Spec.Project, schema.Spec.ServiceName, schema.Spec.SubjectName, - schema.Spec.CompatibilityLevel, + &kafkaschemaregistry.ServiceSchemaRegistrySubjectConfigPutIn{ + Compatibility: schema.Spec.CompatibilityLevel, + }, ) if err != nil { return fmt.Errorf("cannot update Kafka Schema Configuration: %w", err) } } - // get last version - version, err := h.getLastVersion(ctx, avn, schema) + // Gets the last version + // Because of eventual consistency, we must poll the subject + const ( + pollDelay = 5 * time.Second + pollAttempts = 10 + ) + + err = retry.Do( + func() error { + version, err := getKafkaSchemaVersion( + ctx, + avnGen, + schemaID, + schema.Spec.Project, + schema.Spec.ServiceName, + schema.Spec.SubjectName, + ) + if err != nil { + return err + } + schema.Status.Version = version.Version + return nil + }, + retry.Context(ctx), + retry.RetryIf(isNotFound), + retry.Delay(pollDelay), + retry.Attempts(pollAttempts), + ) if err != nil { return fmt.Errorf("cannot get Kafka Schema Subject version: %w", err) } - schema.Status.Version = version - meta.SetStatusCondition(&schema.Status.Conditions, getInitializedCondition("Added", "Successfully created or updated the instance in Aiven")) @@ -105,12 +146,8 @@ func (h KafkaSchemaHandler) delete(ctx context.Context, avn *aiven.Client, avnGe return false, err } - err = avn.KafkaSubjectSchemas.Delete(ctx, schema.Spec.Project, schema.Spec.ServiceName, schema.Spec.SubjectName) - if err != nil && !isNotFound(err) { - return false, fmt.Errorf("aiven client delete Kafka Schema error: %w", err) - } - - return true, nil + err = avnGen.ServiceSchemaRegistrySubjectDelete(ctx, schema.Spec.Project, schema.Spec.ServiceName, schema.Spec.SubjectName) + return isDeleted(err) } func (h KafkaSchemaHandler) get(ctx context.Context, avn *aiven.Client, avnGen avngen.Client, obj client.Object) (*corev1.Secret, error) { @@ -146,22 +183,22 @@ func (h KafkaSchemaHandler) convert(i client.Object) (*v1alpha1.KafkaSchema, err return schema, nil } -func (h KafkaSchemaHandler) getLastVersion(ctx context.Context, avn *aiven.Client, schema *v1alpha1.KafkaSchema) (int, error) { - ver, err := avn.KafkaSubjectSchemas.GetVersions( - ctx, - schema.Spec.Project, - schema.Spec.ServiceName, - schema.Spec.SubjectName) +func getKafkaSchemaVersion(ctx context.Context, client avngen.Client, schemaID int, projectName, serviceName, subjectName string) (*kafkaschemaregistry.ServiceSchemaRegistrySubjectVersionGetOut, error) { + versions, err := client.ServiceSchemaRegistrySubjectVersionsGet(ctx, projectName, serviceName, subjectName) if err != nil { - return 0, err + return nil, err } - var latestVersion int - for _, v := range ver.Versions { - if v > latestVersion { - latestVersion = v + for _, v := range versions { + version, err := client.ServiceSchemaRegistrySubjectVersionGet(ctx, projectName, serviceName, subjectName, v) + if err != nil { + return nil, err + } + + if version.Id == schemaID { + return version, nil } } - return latestVersion, nil + return nil, NewNotFound(fmt.Sprintf("the schema with id %d not found", schemaID)) } diff --git a/docs/docs/api-reference/kafkaschema.md b/docs/docs/api-reference/kafkaschema.md index f2ac1a57..537b10a3 100644 --- a/docs/docs/api-reference/kafkaschema.md +++ b/docs/docs/api-reference/kafkaschema.md @@ -84,6 +84,7 @@ KafkaSchemaSpec defines the desired state of KafkaSchema. - [`authSecretRef`](#spec.authSecretRef-property){: name='spec.authSecretRef-property'} (object). Authentication reference to Aiven token in a secret. See below for [nested schema](#spec.authSecretRef). - [`compatibilityLevel`](#spec.compatibilityLevel-property){: name='spec.compatibilityLevel-property'} (string, Enum: `BACKWARD`, `BACKWARD_TRANSITIVE`, `FORWARD`, `FORWARD_TRANSITIVE`, `FULL`, `FULL_TRANSITIVE`, `NONE`). Kafka Schemas compatibility level. +- [`schemaType`](#spec.schemaType-property){: name='spec.schemaType-property'} (string, Enum: `AVRO`, `JSON`, `PROTOBUF`). Schema type. ## authSecretRef {: #spec.authSecretRef } diff --git a/go.mod b/go.mod index 23435e31..86f46150 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,8 @@ require ( github.com/ClickHouse/clickhouse-go/v2 v2.28.2 github.com/aiven/aiven-go-client/v2 v2.26.0 github.com/aiven/go-api-schemas v1.85.0 - github.com/aiven/go-client-codegen v0.27.0 + github.com/aiven/go-client-codegen v0.30.0 + github.com/avast/retry-go v3.0.0+incompatible github.com/dave/jennifer v1.7.1 github.com/docker/go-units v0.5.0 github.com/ghodss/yaml v1.0.0 diff --git a/go.sum b/go.sum index e436a3f4..bfada1e5 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/aiven/aiven-go-client/v2 v2.26.0 h1:1dBlF0BULbPsRXEEmcKs71AE3VZ+AYt5z github.com/aiven/aiven-go-client/v2 v2.26.0/go.mod h1:KdHfLIlIRZIfCSEBd39j1Q81jlSb6Nd+oCQKqERfnuA= github.com/aiven/go-api-schemas v1.85.0 h1:wpTCQWjTLKQvVQq184u6Ji0ZksDZkNPqS6f6zrZ+nGU= github.com/aiven/go-api-schemas v1.85.0/go.mod h1:/F7Rr8UVErsRxhgGN7CSo+Ac/uAg/OiAVCEKCfm3VAo= -github.com/aiven/go-client-codegen v0.27.0 h1:LSOrvMJUIRCTl2y6auiwtmKE3hzN1S/M5vxHTk58QaI= -github.com/aiven/go-client-codegen v0.27.0/go.mod h1:6AHM+oM2tNc10eOfPeyiTWFObkVz2dZY2gqRkTPQoVo= +github.com/aiven/go-client-codegen v0.30.0 h1:K+FZW4MvA68xMy5B2p85npeZ3qOwhAcDw/GAhmrJyWY= +github.com/aiven/go-client-codegen v0.30.0/go.mod h1:FfbH32Xb+Hx5zeKTIug1Y8SfMeB+AKNRzxgrzkts2oA= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -50,6 +50,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= diff --git a/tests/kafkaschema_test.go b/tests/kafkaschema_test.go index 80f4d46b..c6ca4e8d 100644 --- a/tests/kafkaschema_test.go +++ b/tests/kafkaschema_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/aiven/go-client-codegen/handler/kafkaschemaregistry" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -43,6 +44,7 @@ spec: project: %[1]s serviceName: %[2]s subjectName: %[4]s + schemaType: AVRO compatibilityLevel: BACKWARD schema: | { @@ -104,7 +106,8 @@ func TestKafkaSchema(t *testing.T) { assert.Equal(t, schemaName, schema.Name) assert.Equal(t, subjectName, schema.Spec.SubjectName) assert.Equal(t, kafkaName, schema.Spec.ServiceName) - assert.Equal(t, "BACKWARD", schema.Spec.CompatibilityLevel) + assert.Equal(t, kafkaschemaregistry.SchemaTypeAvro, schema.Spec.SchemaType) + assert.Equal(t, kafkaschemaregistry.CompatibilityTypeBackward, schema.Spec.CompatibilityLevel) type schemaType struct { Default any `json:"default,omitempty"` diff --git a/tests/session.go b/tests/session.go index a9d318ea..dea60544 100644 --- a/tests/session.go +++ b/tests/session.go @@ -361,11 +361,3 @@ func parseObjs(src string) (map[string]client.Object, error) { } return objs, nil } - -func ptrValue[T any](v *T) T { - if v == nil { - var empty T - return empty - } - return *v -}