24
24
import com .devshawn .kafka .gitops .service .KafkaService ;
25
25
import com .devshawn .kafka .gitops .service .ParserService ;
26
26
import com .devshawn .kafka .gitops .service .RoleService ;
27
+ import com .devshawn .kafka .gitops .config .SchemaRegistryConfig ;
28
+ import com .devshawn .kafka .gitops .config .SchemaRegistryConfigLoader ;
29
+ import com .devshawn .kafka .gitops .service .SchemaRegistryService ;
27
30
import com .devshawn .kafka .gitops .util .LogUtil ;
28
31
import com .devshawn .kafka .gitops .util .StateUtil ;
29
32
import com .fasterxml .jackson .core .JsonParser ;
30
33
import com .fasterxml .jackson .databind .DeserializationFeature ;
31
34
import com .fasterxml .jackson .databind .ObjectMapper ;
32
35
import com .fasterxml .jackson .datatype .jdk8 .Jdk8Module ;
36
+ import io .confluent .kafka .schemaregistry .ParsedSchema ;
37
+ import io .confluent .kafka .schemaregistry .SchemaProvider ;
38
+ import io .confluent .kafka .schemaregistry .avro .AvroSchemaProvider ;
39
+ import io .confluent .kafka .schemaregistry .client .rest .entities .SchemaReference ;
33
40
import org .slf4j .LoggerFactory ;
34
41
42
+ import java .nio .file .Files ;
43
+ import java .nio .file .Paths ;
35
44
import java .util .ArrayList ;
45
+ import java .util .Collections ;
36
46
import java .util .List ;
37
47
import java .util .Map ;
38
48
import java .util .NoSuchElementException ;
@@ -48,6 +58,7 @@ public class StateManager {
48
58
private final ObjectMapper objectMapper ;
49
59
private final ParserService parserService ;
50
60
private final KafkaService kafkaService ;
61
+ private final SchemaRegistryService schemaRegistryService ;
51
62
private final RoleService roleService ;
52
63
private final ConfluentCloudService confluentCloudService ;
53
64
@@ -61,17 +72,19 @@ public StateManager(ManagerConfig managerConfig, ParserService parserService) {
61
72
this .managerConfig = managerConfig ;
62
73
this .objectMapper = initializeObjectMapper ();
63
74
this .kafkaService = new KafkaService (KafkaGitopsConfigLoader .load ());
75
+ this .schemaRegistryService = new SchemaRegistryService (SchemaRegistryConfigLoader .load ());
64
76
this .parserService = parserService ;
65
77
this .roleService = new RoleService ();
66
78
this .confluentCloudService = new ConfluentCloudService (objectMapper );
67
- this .planManager = new PlanManager (managerConfig , kafkaService , objectMapper );
68
- this .applyManager = new ApplyManager (managerConfig , kafkaService );
79
+ this .planManager = new PlanManager (managerConfig , kafkaService , schemaRegistryService , objectMapper );
80
+ this .applyManager = new ApplyManager (managerConfig , kafkaService , schemaRegistryService );
69
81
}
70
82
71
83
public DesiredStateFile getAndValidateStateFile () {
72
84
DesiredStateFile desiredStateFile = parserService .parseStateFile ();
73
85
validateTopics (desiredStateFile );
74
86
validateCustomAcls (desiredStateFile );
87
+ validateSchemas (desiredStateFile );
75
88
this .describeAclEnabled = StateUtil .isDescribeTopicAclEnabled (desiredStateFile );
76
89
return desiredStateFile ;
77
90
}
@@ -90,6 +103,7 @@ private DesiredPlan generatePlan() {
90
103
planManager .planAcls (desiredState , desiredPlan );
91
104
}
92
105
planManager .planTopics (desiredState , desiredPlan );
106
+ planManager .planSchemas (desiredState , desiredPlan );
93
107
return desiredPlan .build ();
94
108
}
95
109
@@ -105,6 +119,7 @@ public DesiredPlan apply() {
105
119
if (!managerConfig .isSkipAclsDisabled ()) {
106
120
applyManager .applyAcls (desiredPlan );
107
121
}
122
+ applyManager .applySchemas (desiredPlan );
108
123
109
124
return desiredPlan ;
110
125
}
@@ -145,6 +160,7 @@ private DesiredState getDesiredState() {
145
160
.addAllPrefixedTopicsToIgnore (getPrefixedTopicsToIgnore (desiredStateFile ));
146
161
147
162
generateTopicsState (desiredState , desiredStateFile );
163
+ generateSchemasState (desiredState , desiredStateFile );
148
164
149
165
if (isConfluentCloudEnabled (desiredStateFile )) {
150
166
generateConfluentCloudServiceAcls (desiredState , desiredStateFile );
@@ -169,6 +185,10 @@ private void generateTopicsState(DesiredState.Builder desiredState, DesiredState
169
185
}
170
186
}
171
187
188
+ private void generateSchemasState (DesiredState .Builder desiredState , DesiredStateFile desiredStateFile ) {
189
+ desiredState .putAllSchemas (desiredStateFile .getSchemas ());
190
+ }
191
+
172
192
private void generateConfluentCloudServiceAcls (DesiredState .Builder desiredState , DesiredStateFile desiredStateFile ) {
173
193
List <ServiceAccount > serviceAccounts = confluentCloudService .getServiceAccounts ();
174
194
desiredStateFile .getServices ().forEach ((name , service ) -> {
@@ -321,6 +341,47 @@ private void validateTopics(DesiredStateFile desiredStateFile) {
321
341
}
322
342
}
323
343
344
+ private void validateSchemas (DesiredStateFile desiredStateFile ) {
345
+ if (!desiredStateFile .getSchemas ().isEmpty ()) {
346
+ SchemaRegistryConfig schemaRegistryConfig = SchemaRegistryConfigLoader .load ();
347
+ desiredStateFile .getSchemas ().forEach ((s , schemaDetails ) -> {
348
+ if (!schemaDetails .getType ().equalsIgnoreCase ("Avro" )) {
349
+ throw new ValidationException (String .format ("Schema type %s is currently not supported." , schemaDetails .getType ()));
350
+ }
351
+ if (!Files .exists (Paths .get (schemaRegistryConfig .getConfig ().get ("SCHEMA_DIRECTORY" ) + "/" + schemaDetails .getFile ()))) {
352
+ throw new ValidationException (String .format ("Schema file %s not found in schema directory at %s" , schemaDetails .getFile (), schemaRegistryConfig .getConfig ().get ("SCHEMA_DIRECTORY" )));
353
+ }
354
+ if (schemaDetails .getType ().equalsIgnoreCase ("Avro" )) {
355
+ AvroSchemaProvider avroSchemaProvider = new AvroSchemaProvider ();
356
+ if (schemaDetails .getReferences ().isEmpty () && schemaDetails .getType ().equalsIgnoreCase ("Avro" )) {
357
+ Optional <ParsedSchema > parsedSchema = avroSchemaProvider .parseSchema (schemaRegistryService .loadSchemaFromDisk (schemaDetails .getFile ()), Collections .emptyList ());
358
+ if (!parsedSchema .isPresent ()) {
359
+ throw new ValidationException (String .format ("Avro schema %s could not be parsed." , schemaDetails .getFile ()));
360
+ }
361
+ } else {
362
+ List <SchemaReference > schemaReferences = new ArrayList <>();
363
+ schemaDetails .getReferences ().forEach (referenceDetails -> {
364
+ SchemaReference schemaReference = new SchemaReference (referenceDetails .getName (), referenceDetails .getSubject (), referenceDetails .getVersion ());
365
+ schemaReferences .add (schemaReference );
366
+ });
367
+ // we need to pass a schema registry client as a config because the underlying code validates against the current state
368
+ avroSchemaProvider .configure (Collections .singletonMap (SchemaProvider .SCHEMA_VERSION_FETCHER_CONFIG , schemaRegistryService .createSchemaRegistryClient ()));
369
+ try {
370
+ Optional <ParsedSchema > parsedSchema = avroSchemaProvider .parseSchema (schemaRegistryService .loadSchemaFromDisk (schemaDetails .getFile ()), schemaReferences );
371
+ if (!parsedSchema .isPresent ()) {
372
+ throw new ValidationException (String .format ("Avro schema %s could not be parsed." , schemaDetails .getFile ()));
373
+ }
374
+ } catch (IllegalStateException ex ) {
375
+ throw new ValidationException (String .format ("Reference validation error: %s" , ex .getMessage ()));
376
+ } catch (RuntimeException ex ) {
377
+ throw new ValidationException (String .format ("Error thrown when attempting to validate schema with reference" , ex .getMessage ()));
378
+ }
379
+ }
380
+ }
381
+ });
382
+ }
383
+ }
384
+
324
385
private boolean isConfluentCloudEnabled (DesiredStateFile desiredStateFile ) {
325
386
if (desiredStateFile .getSettings ().isPresent () && desiredStateFile .getSettings ().get ().getCcloud ().isPresent ()) {
326
387
return desiredStateFile .getSettings ().get ().getCcloud ().get ().isEnabled ();
0 commit comments