Skip to content

Commit 1c992be

Browse files
authored
Add support for transformations in resource templates (#59)
1 parent 9496cba commit 1c992be

File tree

11 files changed

+112
-19
lines changed

11 files changed

+112
-19
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ deploy-dev-environment:
2626
kubectl create -f https://github.com/jetstack/cert-manager/releases/download/v1.8.2/cert-manager.yaml || echo "skipping"
2727
kubectl create namespace kafka || echo "skipping"
2828
kubectl create namespace mysql || echo "skipping"
29-
helm repo add flink-operator-repo https://downloads.apache.org/flink/flink-kubernetes-operator-1.4.0/
29+
helm repo add flink-operator-repo https://downloads.apache.org/flink/flink-kubernetes-operator-1.6.1/
3030
helm upgrade --install --atomic --set webhook.create=false flink-kubernetes-operator flink-operator-repo/flink-kubernetes-operator
3131
kubectl apply -f "https://strimzi.io/install/latest?namespace=kafka" -n kafka
3232
kubectl wait --for=condition=Established=True crds/kafkas.kafka.strimzi.io

deploy/dev/kafka.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ metadata:
2323
namespace: kafka
2424
spec:
2525
kafka:
26-
version: 3.4.0
26+
version: 3.6.1
2727
replicas: 1
2828
listeners:
2929
- name: plain

etc/integration-tests.sql

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,20 @@
77
SELECT * FROM DATAGEN.PERSON;
88
SELECT * FROM DATAGEN.COMPANY;
99

10+
-- test mermaid and yaml commands
11+
!mermaid insert into RAWKAFKA."test-sink" SELECT AGE AS PAYLOAD, NAME AS KEY FROM DATAGEN.PERSON
12+
!yaml insert into RAWKAFKA."test-sink" SELECT AGE AS PAYLOAD, NAME AS KEY FROM DATAGEN.PERSON
13+
14+
-- test insert into command
15+
!insert into RAWKAFKA."test-sink" SELECT AGE AS PAYLOAD, NAME AS KEY FROM DATAGEN.PERSON
16+
SELECT * FROM RAWKAFKA."test-sink" LIMIT 5;
17+
1018
-- MySQL CDC tables
1119
SELECT * FROM INVENTORY."products_on_hand" LIMIT 1;
1220

1321
-- Test check command
1422
!check not empty SELECT * FROM INVENTORY."products_on_hand";
1523

16-
-- MySQL CDC -> Kafka
24+
-- MySQL CDC -> Kafka (via sample subscription "products")
1725
SELECT * FROM RAWKAFKA."products" LIMIT 1;
1826

19-
-- test insert into command
20-
!insert into RAWKAFKA."test-sink" SELECT AGE AS PAYLOAD, NAME AS KEY FROM DATAGEN.PERSON
21-
SELECT * FROM RAWKAFKA."test-sink" LIMIT 5;
22-
23-
-- test mermaid and yaml commands
24-
!mermaid insert into RAWKAFKA."test-sink" SELECT AGE AS PAYLOAD, NAME AS KEY FROM DATAGEN.PERSON
25-
!yaml insert into RAWKAFKA."test-sink" SELECT AGE AS PAYLOAD, NAME AS KEY FROM DATAGEN.PERSON
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.linkedin.hoptimator.catalog;
2+
3+
import java.util.Locale;
4+
5+
public final class Names {
6+
7+
private Names() {
8+
}
9+
10+
/** Attempt to format s as a K8s object name, or part of one. */
11+
public static String canonicalize(String s) {
12+
return s.toLowerCase(Locale.ROOT)
13+
.replaceAll("[^a-z0-9\\-]+", "-")
14+
.replaceAll("^[^a-z0-9]*", "")
15+
.replaceAll("[^a-z0-9]*$", "")
16+
.replaceAll("\\-+", "-");
17+
}
18+
19+
}

hoptimator-catalog/src/main/java/com/linkedin/hoptimator/catalog/Resource.java

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import java.io.InputStream;
44
import java.io.IOException;
5+
import java.util.Arrays;
56
import java.util.ArrayList;
67
import java.util.Collection;
78
import java.util.Enumeration;
89
import java.util.HashMap;
910
import java.util.List;
11+
import java.util.Locale;
1012
import java.util.Map;
1113
import java.util.Properties;
1214
import java.util.Set;
@@ -255,7 +257,21 @@ public interface Template {
255257
* Replaces `{{var}}` in a template file with the corresponding variable.
256258
*
257259
* Resource-scoped variables take precedence over Environment-scoped
258-
* variables. Default values can supplied with `{{var:default}}`.
260+
* variables.
261+
*
262+
* Default values can supplied with `{{var:default}}`.
263+
*
264+
* Built-in transformations can be applied to variables, including:
265+
*
266+
* - `{{var toName}}`, `{{var:default toName}}`: canonicalize the
267+
* variable as a valid K8s object name.
268+
* - `{{var toUpperCase}}`, `{{var:default toUpperCase}}`: render in
269+
* all upper case.
270+
* - `{{var toLowerCase}}`, `{{var:default toLowerCase}}`: render in
271+
* all lower case.
272+
* - `{{var concat}}`, `{{var:default concat}}`: concatinate a multiline
273+
* string into one line
274+
* - `{{var concat toUpperCase}}`: apply both transformations in sequence.
259275
*
260276
* If `var` contains multiple lines, the behavior depends on context;
261277
* specifically, whether the pattern appears within a list or comment
@@ -288,7 +304,8 @@ public SimpleTemplate(Environment env, String template) {
288304
@Override
289305
public String render(Resource resource) {
290306
StringBuffer sb = new StringBuffer();
291-
Pattern p = Pattern.compile("([\\s\\-\\#]*)\\{\\{\\s*([\\w_\\-\\.]+)\\s*(:([\\w_\\-\\.]+))?\\s*\\}\\}");
307+
Pattern p = Pattern.compile(
308+
"([\\s\\-\\#]*)\\{\\{\\s*([\\w_\\-\\.]+)\\s*(:([\\w_\\-\\.]+))?\\s*((\\w+\\s*)*)\\s*\\}\\}");
292309
Matcher m = p.matcher(template);
293310
while (m.find()) {
294311
String prefix = m.group(1);
@@ -297,18 +314,47 @@ public String render(Resource resource) {
297314
}
298315
String key = m.group(2);
299316
String defaultValue = m.group(4);
317+
String transform = m.group(5);
300318
String value = resource.getOrDefault(key, () -> env.getOrDefault(key, () -> defaultValue));
301319
if (value == null) {
302320
throw new IllegalArgumentException(template + " has no value for key " + key + ".");
303321
}
322+
String transformedValue = applyTransform(value, transform);
304323
String quotedPrefix = Matcher.quoteReplacement(prefix);
305-
String quotedValue = Matcher.quoteReplacement(value);
324+
String quotedValue = Matcher.quoteReplacement(transformedValue);
306325
String replacement = quotedPrefix + quotedValue.replaceAll("\\n", quotedPrefix);
307326
m.appendReplacement(sb, replacement);
308327
}
309328
m.appendTail(sb);
310329
return sb.toString();
311330
}
331+
332+
private static String applyTransform(String value, String transform) {
333+
String res = value;
334+
String[] funcs = transform.split("\\W+");
335+
for (String f : funcs) {
336+
switch (f) {
337+
case "toLowerCase":
338+
res = res.toLowerCase(Locale.ROOT);
339+
break;
340+
case "toUpperCase":
341+
res = res.toUpperCase(Locale.ROOT);
342+
break;
343+
case "toName":
344+
res = canonicalizeName(res);
345+
break;
346+
case "concat":
347+
res = res.replace("\n", "");
348+
break;
349+
}
350+
}
351+
return res;
352+
}
353+
354+
/** Attempt to format s as a K8s object name, or part of one. */
355+
protected static String canonicalizeName(String s) {
356+
return Names.canonicalize(s);
357+
}
312358
}
313359

314360
/** Locates a Template for a given Resource */

hoptimator-catalog/src/test/java/com/linkedin/hoptimator/catalog/ResourceTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import static org.junit.Assert.assertEquals;
66
import org.junit.Test;
77

8+
import java.util.function.Function;
9+
810
public class ResourceTest {
911

1012
@Test
@@ -25,4 +27,26 @@ public void handlesChainedEnvironments() {
2527
assertEquals("bar", env.getOrDefault("foo", () -> "x"));
2628
assertEquals("x", env.getOrDefault("oof", () -> "x"));
2729
}
30+
31+
@Test
32+
public void rendersTemplates() {
33+
Resource.Environment env = new Resource.SimpleEnvironment() {{
34+
export("one", "1");
35+
export("foo", "bar");
36+
}};
37+
Resource res = new Resource("x") {{
38+
export("car", "Hyundai Accent");
39+
export("parts", "wheels\nseats\nbrakes\nwipers");
40+
}};
41+
42+
Function<String, Resource.Template> f = x -> new Resource.SimpleTemplate(env, x);
43+
assertEquals("xyz", f.apply("xyz").render(res));
44+
assertEquals("bar", f.apply("{{foo}}").render(res));
45+
assertEquals("bar", f.apply("{{ foo }}").render(res));
46+
assertEquals("abc", f.apply("{{xyz:abc}}").render(res));
47+
assertEquals("hyundai-accent", f.apply("{{car toName}}").render(res));
48+
assertEquals("HYUNDAI-ACCENT", f.apply("{{car toName toUpperCase}}").render(res));
49+
assertEquals("WHEELSSEATSBRAKESWIPERS", f.apply("{{parts concat toUpperCase}}").render(res));
50+
assertEquals("BAR\nbar\nxyz", f.apply("{{foo toUpperCase}}\n{{foo toLowerCase}}\n{{x:xyz}}").render(res));
51+
}
2852
}

hoptimator-kafka-adapter/src/main/java/com/linkedin/hoptimator/catalog/kafka/KafkaTopic.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22

33
import com.linkedin.hoptimator.catalog.Resource;
44

5-
import java.util.Locale;
65
import java.util.Map;
76

87
class KafkaTopic extends Resource {
98
KafkaTopic(String topicName, Map<String, String> clientOverrides) {
109
super("KafkaTopic");
1110
export("topicName", topicName);
12-
export("topicNameLowerCase", topicName.toLowerCase(Locale.ROOT));
1311
export("clientOverrides", clientOverrides);
1412
}
1513
}

hoptimator-kafka-adapter/src/main/java/com/linkedin/hoptimator/catalog/kafka/KafkaTopicAcl.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ class KafkaTopicAcl extends Resource {
88
public KafkaTopicAcl(String topicName, String principal, String method) {
99
super("KafkaTopicAcl");
1010
export("topicName", topicName);
11-
export("topicNameLowerCase", topicName.toLowerCase(Locale.ROOT));
1211
export("principal", principal);
1312
export("method", method);
1413
}

hoptimator-kafka-adapter/src/main/resources/KafkaTopic.yaml.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
apiVersion: hoptimator.linkedin.com/v1alpha1
22
kind: KafkaTopic
33
metadata:
4-
name: {{topicNameLowerCase}}
4+
name: {{topicName toName}}
55
namespace: {{pipeline.namespace}}
66
spec:
77
topicName: {{topicName}}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
apiVersion: hoptimator.linkedin.com/v1alpha1
22
kind: Acl
33
metadata:
4-
name: {{topicNameLowerCase}}-acl-{{id}}
4+
name: {{topicName toName}}-acl-{{id}}
55
namespace: {{pipeline.namespace}}
66
spec:
77
resource:
88
kind: KafkaTopic
9-
name: {{topicNameLowerCase}}
9+
name: {{topicName toName}}
1010
method: {{method}}
1111
principal: {{principal}}

0 commit comments

Comments
 (0)