Skip to content

Commit 71d6189

Browse files
committed
Add tests for REST API, update docs and RoutingRule class
1 parent 765da61 commit 71d6189

File tree

6 files changed

+242
-27
lines changed

6 files changed

+242
-27
lines changed

docs/gateway-api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,11 @@ curl -X POST http://localhost:8080/gateway/backend/activate/trino-2
9696
This API can be used to programmatically update the Routing Rules.
9797
Rule will be updated based on the rule name.
9898

99-
For this feature to work with multiple replicas of the Trino Gateway, you will need to provide a shared storage for the routing rules file. If multiple replicas are used with local storage, then rules will get out of sync when updated.
99+
For this feature to work with multiple replicas of the Trino Gateway, you will need to provide a shared storage that supports file locking for the routing rules file. If multiple replicas are used with local storage, then rules will get out of sync when updated.
100100

101101
```shell
102102
curl -X POST http://localhost:8080/webapp/updateRoutingRules \
103+
-H 'Content-Type: application/json' \
103104
-d '{ "name": "trino-rule",
104105
"description": "updated rule description",
105106
"priority": 0,

gateway-ha/src/main/java/io/trino/gateway/ha/domain/RoutingRule.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
package io.trino.gateway.ha.domain;
1515

1616
import com.google.common.collect.ImmutableList;
17-
import jakarta.annotation.Nullable;
1817

1918
import java.util.List;
2019

2120
import static java.util.Objects.requireNonNull;
21+
import static java.util.Objects.requireNonNullElse;
2222

2323
/**
2424
* RoutingRules
@@ -31,13 +31,15 @@
3131
*/
3232
public record RoutingRule(
3333
String name,
34-
@Nullable String description,
35-
@Nullable Integer priority,
34+
String description,
35+
Integer priority,
3636
List<String> actions,
3737
String condition)
3838
{
3939
public RoutingRule {
4040
requireNonNull(name, "name is null");
41+
requireNonNullElse(description, "");
42+
requireNonNullElse(priority, 0);
4143
actions = ImmutableList.copyOf(actions);
4244
requireNonNull(condition, "condition is null");
4345
}

gateway-ha/src/main/java/io/trino/gateway/ha/router/RoutingRulesManager.java

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import java.nio.file.Files;
2828
import java.nio.file.Paths;
2929
import java.nio.file.StandardOpenOption;
30-
import java.util.ArrayList;
3130
import java.util.List;
3231

3332
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -65,32 +64,23 @@ public synchronized List<RoutingRule> updateRoutingRule(RoutingRule routingRule)
6564
throws IOException
6665
{
6766
ImmutableList.Builder<RoutingRule> updatedRoutingRulesBuilder = ImmutableList.builder();
68-
List<RoutingRule> currentRoutingRulesList = new ArrayList<>();
69-
ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
70-
try {
71-
String content = Files.readString(Paths.get(rulesConfigPath), UTF_8);
72-
YAMLParser parser = new YAMLFactory().createParser(content);
73-
while (parser.nextToken() != null) {
74-
RoutingRule currentRoutingRule = yamlReader.readValue(parser, RoutingRule.class);
75-
currentRoutingRulesList.add(currentRoutingRule);
76-
}
77-
for (int i = 0; i < currentRoutingRulesList.size(); i++) {
78-
if (currentRoutingRulesList.get(i).name().equals(routingRule.name())) {
79-
currentRoutingRulesList.set(i, routingRule);
80-
break;
81-
}
82-
}
67+
List<RoutingRule> currentRoutingRulesList = getRoutingRules();
68+
try (FileChannel fileChannel = FileChannel.open(Paths.get(rulesConfigPath), StandardOpenOption.WRITE, StandardOpenOption.READ);
69+
FileLock lock = fileChannel.lock()) {
8370
ObjectMapper yamlWriter = new ObjectMapper(new YAMLFactory());
8471
StringBuilder yamlContent = new StringBuilder();
8572
for (RoutingRule rule : currentRoutingRulesList) {
86-
yamlContent.append(yamlWriter.writeValueAsString(rule));
87-
updatedRoutingRulesBuilder.add(rule);
88-
}
89-
try (FileChannel fileChannel = FileChannel.open(Paths.get(rulesConfigPath), StandardOpenOption.WRITE, StandardOpenOption.READ);
90-
FileLock lock = fileChannel.lock()) {
91-
Files.writeString(Paths.get(rulesConfigPath), yamlContent.toString(), UTF_8);
92-
lock.release();
73+
if (rule.name().equals(routingRule.name())) {
74+
yamlContent.append(yamlWriter.writeValueAsString(routingRule));
75+
updatedRoutingRulesBuilder.add(routingRule);
76+
}
77+
else {
78+
yamlContent.append(yamlWriter.writeValueAsString(rule));
79+
updatedRoutingRulesBuilder.add(rule);
80+
}
9381
}
82+
Files.writeString(Paths.get(rulesConfigPath), yamlContent.toString(), UTF_8);
83+
lock.release();
9484
}
9585
catch (IOException e) {
9686
throw new IOException("Failed to parse or update routing rules configuration form path : " + rulesConfigPath, e);
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package io.trino.gateway.ha.router;
15+
16+
import com.fasterxml.jackson.databind.JsonNode;
17+
import com.fasterxml.jackson.databind.ObjectMapper;
18+
import io.airlift.json.JsonCodec;
19+
import io.trino.gateway.ha.HaGatewayLauncher;
20+
import io.trino.gateway.ha.HaGatewayTestUtils;
21+
import io.trino.gateway.ha.config.UIConfiguration;
22+
import io.trino.gateway.ha.domain.RoutingRule;
23+
import okhttp3.MediaType;
24+
import okhttp3.OkHttpClient;
25+
import okhttp3.Request;
26+
import okhttp3.RequestBody;
27+
import okhttp3.Response;
28+
import org.junit.jupiter.api.BeforeAll;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.TestInstance;
31+
import org.testcontainers.containers.TrinoContainer;
32+
33+
import java.util.List;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.testcontainers.utility.MountableFile.forClasspathResource;
37+
38+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
39+
final class TestRoutingAPI
40+
{
41+
private final OkHttpClient httpClient = new OkHttpClient();
42+
private TrinoContainer trino;
43+
int routerPort = 21001 + (int) (Math.random() * 1000);
44+
int backendPort;
45+
46+
@BeforeAll
47+
void setup()
48+
throws Exception
49+
{
50+
trino = new TrinoContainer("trinodb/trino");
51+
trino.withCopyFileToContainer(forClasspathResource("trino-config.properties"), "/etc/trino/config.properties");
52+
trino.start();
53+
54+
backendPort = trino.getMappedPort(8080);
55+
56+
// seed database
57+
HaGatewayTestUtils.TestConfig testConfig =
58+
HaGatewayTestUtils.buildGatewayConfigAndSeedDb(routerPort, "test-config-with-routing-rules-api.yml");
59+
// Start Gateway
60+
String[] args = {testConfig.configFilePath()};
61+
HaGatewayLauncher.main(args);
62+
// Now populate the backend
63+
HaGatewayTestUtils.setUpBackend(
64+
"trino1", "http://localhost:" + backendPort, "externalUrl", true, "adhoc", routerPort);
65+
}
66+
67+
@Test
68+
void testGetRoutingRulesAPI()
69+
throws Exception
70+
{
71+
Request request =
72+
new Request.Builder()
73+
.url("http://localhost:" + routerPort + "/webapp/getRoutingRules")
74+
.get()
75+
.build();
76+
Response response = httpClient.newCall(request).execute();
77+
78+
String responseBody = response.body().string();
79+
ObjectMapper objectMapper = new ObjectMapper();
80+
JsonNode rootNode = objectMapper.readTree(responseBody);
81+
JsonNode dataNode = rootNode.path("data");
82+
83+
JsonCodec<RoutingRule[]> responseCodec = JsonCodec.jsonCodec(RoutingRule[].class);
84+
RoutingRule[] routingRules = responseCodec.fromJson(dataNode.toString());
85+
86+
assertThat(response.code()).isEqualTo(200);
87+
assertThat(routingRules[0].name()).isEqualTo("airflow");
88+
assertThat(routingRules[0].description()).isEqualTo("if query from airflow, route to etl group");
89+
assertThat(routingRules[0].priority()).isEqualTo(0);
90+
assertThat(routingRules[0].condition()).isEqualTo("request.getHeader(\"X-Trino-Source\") == \"airflow\"");
91+
assertThat(routingRules[0].actions()).first().isEqualTo("result.put(\"routingGroup\", \"etl\")");
92+
}
93+
94+
@Test
95+
void testUpdateRoutingRulesAPI()
96+
throws Exception
97+
{
98+
//Update routing rules with a new rule
99+
RoutingRule updatedRoutingRules = new RoutingRule("airflow", "if query from airflow, route to adhoc group", 0, List.of("result.put(\"routingGroup\", \"adhoc\")"), "request.getHeader(\"X-Trino-Source\") == \"JDBC\"");
100+
ObjectMapper objectMapper = new ObjectMapper();
101+
RequestBody requestBody = RequestBody.create(objectMapper.writeValueAsString(updatedRoutingRules), MediaType.parse("application/json; charset=utf-8"));
102+
Request request = new Request.Builder()
103+
.url("http://localhost:" + routerPort + "/webapp/updateRoutingRules")
104+
.addHeader("Content-Type", "application/json")
105+
.post(requestBody)
106+
.build();
107+
Response response = httpClient.newCall(request).execute();
108+
109+
assertThat(response.code()).isEqualTo(200);
110+
111+
//Fetch the routing rules to see if the update was successful
112+
Request request2 =
113+
new Request.Builder()
114+
.url("http://localhost:" + routerPort + "/webapp/getRoutingRules")
115+
.get()
116+
.build();
117+
Response response2 = httpClient.newCall(request2).execute();
118+
119+
String responseBody = response2.body().string();
120+
ObjectMapper objectMapper2 = new ObjectMapper();
121+
JsonNode rootNode = objectMapper2.readTree(responseBody);
122+
JsonNode dataNode = rootNode.path("data");
123+
124+
JsonCodec<RoutingRule[]> responseCodec = JsonCodec.jsonCodec(RoutingRule[].class);
125+
RoutingRule[] routingRules = responseCodec.fromJson(dataNode.toString());
126+
127+
assertThat(response.code()).isEqualTo(200);
128+
assertThat(routingRules[0].name()).isEqualTo("airflow");
129+
assertThat(routingRules[0].description()).isEqualTo("if query from airflow, route to adhoc group");
130+
assertThat(routingRules[0].priority()).isEqualTo(0);
131+
assertThat(routingRules[0].condition()).isEqualTo("request.getHeader(\"X-Trino-Source\") == \"JDBC\"");
132+
assertThat(routingRules[0].actions()).first().isEqualTo("result.put(\"routingGroup\", \"adhoc\")");
133+
134+
//Revert back to old routing rules to avoid any test failures
135+
RoutingRule revertRoutingRules = new RoutingRule("airflow", "if query from airflow, route to etl group", 0, List.of("result.put(\"routingGroup\", \"etl\")"), "request.getHeader(\"X-Trino-Source\") == \"airflow\"");
136+
ObjectMapper objectMapper3 = new ObjectMapper();
137+
RequestBody requestBody3 = RequestBody.create(objectMapper3.writeValueAsString(revertRoutingRules), MediaType.parse("application/json; charset=utf-8"));
138+
Request request3 = new Request.Builder()
139+
.url("http://localhost:" + routerPort + "/webapp/updateRoutingRules")
140+
.addHeader("Content-Type", "application/json")
141+
.post(requestBody3)
142+
.build();
143+
httpClient.newCall(request3).execute();
144+
}
145+
146+
@Test
147+
void testUIConfigurationAPI()
148+
throws Exception
149+
{
150+
Request request = new Request.Builder()
151+
.url("http://localhost:" + routerPort + "/webapp/getUIConfiguration")
152+
.get()
153+
.build();
154+
155+
Response response = httpClient.newCall(request).execute();
156+
String responseBody = response.body().string();
157+
158+
ObjectMapper objectMapper2 = new ObjectMapper();
159+
JsonNode rootNode = objectMapper2.readTree(responseBody);
160+
JsonNode dataNode = rootNode.path("data");
161+
162+
ObjectMapper objectMapper = new ObjectMapper();
163+
UIConfiguration uiConfiguration = objectMapper.readValue(dataNode.toString(), UIConfiguration.class);
164+
165+
assertThat(response.code()).isEqualTo(200);
166+
assertThat(uiConfiguration.getDisablePages()).contains("routing-rules");
167+
}
168+
}

gateway-ha/src/test/resources/rules/routing_rules_update.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,11 @@ priority: 0
55
actions:
66
- "result.put(\"routingGroup\", \"etl\")"
77
condition: "request.getHeader(\"X-Trino-Source\") == \"airflow\""
8+
---
9+
name: "airflow special"
10+
description: "if query from airflow with special label, route to etl-special group"
11+
priority: 1
12+
actions:
13+
- "result.put(\"routingGroup\", \"etl-special\")"
14+
condition: "request.getHeader(\"X-Trino-Source\") == \"airflow\" && request.getHeader(\"\
15+
X-Trino-Client-Tags\") contains \"label=special\""
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
serverConfig:
2+
node.environment: test
3+
http-server.http.port: REQUEST_ROUTER_PORT
4+
5+
dataStore:
6+
jdbcUrl: jdbc:h2:DB_FILE_PATH
7+
user: sa
8+
password: sa
9+
driver: org.h2.Driver
10+
11+
modules:
12+
- io.trino.gateway.ha.module.HaGatewayProviderModule
13+
- io.trino.gateway.ha.module.ClusterStateListenerModule
14+
- io.trino.gateway.ha.module.ClusterStatsMonitorModule
15+
16+
managedApps:
17+
- io.trino.gateway.ha.clustermonitor.ActiveClusterMonitor
18+
19+
clusterStatsConfiguration:
20+
monitorType: INFO_API
21+
22+
monitor:
23+
taskDelaySeconds: 1
24+
25+
extraWhitelistPaths:
26+
- '/v1/custom.*'
27+
- '/custom/logout.*'
28+
29+
gatewayCookieConfiguration:
30+
enabled: true
31+
cookieSigningSecret: "kjlhbfrewbyuo452cds3dc1234ancdsjh"
32+
33+
oauth2GatewayCookieConfiguration:
34+
deletePaths:
35+
- "/custom/logout"
36+
37+
requestAnalyzerConfig:
38+
analyzeRequest: true
39+
40+
uiConfiguration:
41+
disablePages:
42+
- 'routing-rules'
43+
44+
routingRules:
45+
rulesEngineEnabled: true
46+
rulesConfigPath: "RESOURCES_DIR/rules/routing_rules_update.yml"

0 commit comments

Comments
 (0)