Skip to content

Commit 4f77442

Browse files
authored
Show and modify routing rules from the UI
1 parent bd2f513 commit 4f77442

File tree

19 files changed

+913
-24
lines changed

19 files changed

+913
-24
lines changed

docs/gateway-api.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,37 @@ Will return a JSON array of active Trino cluster backends:
9191
curl -X POST http://localhost:8080/gateway/backend/activate/trino-2
9292
```
9393

94+
## Update Routing Rules
95+
96+
This API can be used to programmatically update the Routing Rules.
97+
Rule will be updated based on the rule name.
98+
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.
100+
101+
```shell
102+
curl -X POST http://localhost:8080/webapp/updateRoutingRules \
103+
-H 'Content-Type: application/json' \
104+
-d '{ "name": "trino-rule",
105+
"description": "updated rule description",
106+
"priority": 0,
107+
"actions": ["updated action"],
108+
"condition": "updated condition"
109+
}'
110+
```
111+
### Disable Routing Rules UI
112+
113+
You can set the `disablePages` config to disable pages on the UI.
114+
115+
The following pages are available:
116+
- `dashboard`
117+
- `cluster`
118+
- `resource-group`
119+
- `selector`
120+
- `history`
121+
- `routing-rules`
122+
123+
```yaml
124+
uiConfiguration:
125+
disablePages:
126+
- 'routing-rules'
127+
```

gateway-ha/src/main/java/io/trino/gateway/baseapp/BaseApp.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import io.trino.gateway.ha.resource.PublicResource;
3636
import io.trino.gateway.ha.resource.TrinoResource;
3737
import io.trino.gateway.ha.router.ForRouter;
38+
import io.trino.gateway.ha.router.RoutingRulesManager;
3839
import io.trino.gateway.ha.security.AuthorizedExceptionMapper;
3940
import io.trino.gateway.proxyserver.ForProxy;
4041
import io.trino.gateway.proxyserver.ProxyRequestHandler;
@@ -144,6 +145,7 @@ public void configure(Binder binder)
144145
jaxrsBinder(binder).bind(AuthorizedExceptionMapper.class);
145146
binder.bind(ProxyHandlerStats.class).in(Scopes.SINGLETON);
146147
newExporter(binder).export(ProxyHandlerStats.class).withGeneratedName();
148+
binder.bind(RoutingRulesManager.class);
147149
}
148150

149151
private static void addManagedApps(HaGatewayConfiguration configuration, Binder binder)

gateway-ha/src/main/java/io/trino/gateway/ha/config/HaGatewayConfiguration.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public class HaGatewayConfiguration
4646

4747
private RequestAnalyzerConfig requestAnalyzerConfig = new RequestAnalyzerConfig();
4848

49+
private UIConfiguration uiConfiguration = new UIConfiguration();
50+
4951
// List of Modules with FQCN (Fully Qualified Class Name)
5052
private List<String> modules;
5153

@@ -214,6 +216,16 @@ public void setRequestAnalyzerConfig(RequestAnalyzerConfig requestAnalyzerConfig
214216
this.requestAnalyzerConfig = requestAnalyzerConfig;
215217
}
216218

219+
public UIConfiguration getUiConfiguration()
220+
{
221+
return uiConfiguration;
222+
}
223+
224+
public void setUiConfiguration(UIConfiguration uiConfiguration)
225+
{
226+
this.uiConfiguration = uiConfiguration;
227+
}
228+
217229
public List<String> getModules()
218230
{
219231
return this.modules;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.config;
15+
16+
import com.fasterxml.jackson.annotation.JsonProperty;
17+
18+
import java.util.List;
19+
20+
public class UIConfiguration
21+
{
22+
private List<String> disablePages;
23+
24+
@JsonProperty
25+
public List<String> getDisablePages()
26+
{
27+
return disablePages;
28+
}
29+
30+
public void setDisablePages(List<String> disablePages)
31+
{
32+
this.disablePages = disablePages;
33+
}
34+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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.domain;
15+
16+
import com.google.common.collect.ImmutableList;
17+
18+
import java.util.List;
19+
20+
import static java.util.Objects.requireNonNull;
21+
import static java.util.Objects.requireNonNullElse;
22+
23+
/**
24+
* RoutingRules
25+
*
26+
* @param name name of the routing rule
27+
* @param description description of the routing rule. Defaults to an empty string if not provided, indicating the user intends it to be blank.
28+
* @param priority priority of the routing rule. Higher number represents higher priority. If two rules have same priority then order of execution is not guaranteed.
29+
* @param actions actions of the routing rule
30+
* @param condition condition of the routing rule
31+
*/
32+
public record RoutingRule(
33+
String name,
34+
String description,
35+
Integer priority,
36+
List<String> actions,
37+
String condition)
38+
{
39+
public RoutingRule {
40+
requireNonNull(name, "name is null");
41+
description = requireNonNullElse(description, "");
42+
priority = requireNonNullElse(priority, 0);
43+
actions = ImmutableList.copyOf(actions);
44+
requireNonNull(condition, "condition is null");
45+
}
46+
}

gateway-ha/src/main/java/io/trino/gateway/ha/resource/GatewayWebAppResource.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
import com.google.common.base.Strings;
1717
import com.google.inject.Inject;
1818
import io.trino.gateway.ha.clustermonitor.ClusterStats;
19+
import io.trino.gateway.ha.config.HaGatewayConfiguration;
1920
import io.trino.gateway.ha.config.ProxyBackendConfiguration;
21+
import io.trino.gateway.ha.config.UIConfiguration;
2022
import io.trino.gateway.ha.domain.Result;
23+
import io.trino.gateway.ha.domain.RoutingRule;
2124
import io.trino.gateway.ha.domain.TableData;
2225
import io.trino.gateway.ha.domain.request.GlobalPropertyRequest;
2326
import io.trino.gateway.ha.domain.request.QueryDistributionRequest;
@@ -34,8 +37,10 @@
3437
import io.trino.gateway.ha.router.HaGatewayManager;
3538
import io.trino.gateway.ha.router.QueryHistoryManager;
3639
import io.trino.gateway.ha.router.ResourceGroupsManager;
40+
import io.trino.gateway.ha.router.RoutingRulesManager;
3741
import jakarta.annotation.security.RolesAllowed;
3842
import jakarta.ws.rs.Consumes;
43+
import jakarta.ws.rs.GET;
3944
import jakarta.ws.rs.POST;
4045
import jakarta.ws.rs.Path;
4146
import jakarta.ws.rs.Produces;
@@ -44,6 +49,7 @@
4449
import jakarta.ws.rs.core.Response;
4550
import jakarta.ws.rs.core.SecurityContext;
4651

52+
import java.io.IOException;
4753
import java.time.LocalDateTime;
4854
import java.time.ZoneId;
4955
import java.time.ZoneOffset;
@@ -67,18 +73,25 @@ public class GatewayWebAppResource
6773
private final QueryHistoryManager queryHistoryManager;
6874
private final BackendStateManager backendStateManager;
6975
private final ResourceGroupsManager resourceGroupsManager;
76+
// TODO Avoid putting mutable objects in fields
77+
private final UIConfiguration uiConfiguration;
78+
private final RoutingRulesManager routingRulesManager;
7079

7180
@Inject
7281
public GatewayWebAppResource(
7382
GatewayBackendManager gatewayBackendManager,
7483
QueryHistoryManager queryHistoryManager,
7584
BackendStateManager backendStateManager,
76-
ResourceGroupsManager resourceGroupsManager)
85+
ResourceGroupsManager resourceGroupsManager,
86+
RoutingRulesManager routingRulesManager,
87+
HaGatewayConfiguration configuration)
7788
{
7889
this.gatewayBackendManager = requireNonNull(gatewayBackendManager, "gatewayBackendManager is null");
7990
this.queryHistoryManager = requireNonNull(queryHistoryManager, "queryHistoryManager is null");
8091
this.backendStateManager = requireNonNull(backendStateManager, "backendStateManager is null");
8192
this.resourceGroupsManager = requireNonNull(resourceGroupsManager, "resourceGroupsManager is null");
93+
this.uiConfiguration = configuration.getUiConfiguration();
94+
this.routingRulesManager = requireNonNull(routingRulesManager, "routingRulesManager is null");
8295
}
8396

8497
@POST
@@ -425,4 +438,36 @@ public Response readExactMatchSourceSelector()
425438
List<ResourceGroupsManager.ExactSelectorsDetail> selectorsDetailList = resourceGroupsManager.readExactMatchSourceSelector();
426439
return Response.ok(Result.ok(selectorsDetailList)).build();
427440
}
441+
442+
@GET
443+
@RolesAllowed("ADMIN")
444+
@Produces(MediaType.APPLICATION_JSON)
445+
@Path("/getRoutingRules")
446+
public Response getRoutingRules()
447+
throws IOException
448+
{
449+
List<RoutingRule> routingRulesList = routingRulesManager.getRoutingRules();
450+
return Response.ok(Result.ok(routingRulesList)).build();
451+
}
452+
453+
@POST
454+
@RolesAllowed("ADMIN")
455+
@Consumes(MediaType.APPLICATION_JSON)
456+
@Produces(MediaType.APPLICATION_JSON)
457+
@Path("/updateRoutingRules")
458+
public Response updateRoutingRules(RoutingRule routingRule)
459+
throws IOException
460+
{
461+
List<RoutingRule> routingRulesList = routingRulesManager.updateRoutingRule(routingRule);
462+
return Response.ok(Result.ok(routingRulesList)).build();
463+
}
464+
465+
@GET
466+
@RolesAllowed("ADMIN")
467+
@Produces(MediaType.APPLICATION_JSON)
468+
@Path("/getUIConfiguration")
469+
public Response getUIConfiguration()
470+
{
471+
return Response.ok(Result.ok(uiConfiguration)).build();
472+
}
428473
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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.ObjectMapper;
17+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
18+
import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
19+
import com.google.common.collect.ImmutableList;
20+
import com.google.inject.Inject;
21+
import io.trino.gateway.ha.config.HaGatewayConfiguration;
22+
import io.trino.gateway.ha.domain.RoutingRule;
23+
24+
import java.io.IOException;
25+
import java.io.UncheckedIOException;
26+
import java.nio.channels.FileChannel;
27+
import java.nio.channels.FileLock;
28+
import java.nio.file.Files;
29+
import java.nio.file.Path;
30+
import java.util.List;
31+
32+
import static java.nio.charset.StandardCharsets.UTF_8;
33+
import static java.nio.file.StandardOpenOption.READ;
34+
import static java.nio.file.StandardOpenOption.WRITE;
35+
36+
public class RoutingRulesManager
37+
{
38+
private final String rulesConfigPath;
39+
40+
@Inject
41+
public RoutingRulesManager(HaGatewayConfiguration configuration)
42+
{
43+
this.rulesConfigPath = configuration.getRoutingRules().getRulesConfigPath();
44+
}
45+
46+
public List<RoutingRule> getRoutingRules()
47+
{
48+
YAMLFactory yamlFactory = new YAMLFactory();
49+
ObjectMapper yamlReader = new ObjectMapper(yamlFactory);
50+
ImmutableList.Builder<RoutingRule> routingRulesBuilder = ImmutableList.builder();
51+
try {
52+
String content = Files.readString(Path.of(rulesConfigPath), UTF_8);
53+
YAMLParser parser = yamlFactory.createParser(content);
54+
while (parser.nextToken() != null) {
55+
RoutingRule routingRule = yamlReader.readValue(parser, RoutingRule.class);
56+
routingRulesBuilder.add(routingRule);
57+
}
58+
return routingRulesBuilder.build();
59+
}
60+
catch (IOException e) {
61+
throw new UncheckedIOException("Failed to read or parse routing rules configuration from path : " + rulesConfigPath, e);
62+
}
63+
}
64+
65+
public synchronized List<RoutingRule> updateRoutingRule(RoutingRule routingRule)
66+
{
67+
ImmutableList.Builder<RoutingRule> updatedRoutingRulesBuilder = ImmutableList.builder();
68+
List<RoutingRule> currentRoutingRulesList = getRoutingRules();
69+
Path path = Path.of(rulesConfigPath);
70+
try (FileChannel fileChannel = FileChannel.open(path, WRITE, READ);
71+
FileLock lock = fileChannel.lock()) {
72+
ObjectMapper yamlWriter = new ObjectMapper(new YAMLFactory());
73+
StringBuilder yamlContent = new StringBuilder();
74+
for (RoutingRule rule : currentRoutingRulesList) {
75+
if (rule.name().equals(routingRule.name())) {
76+
yamlContent.append(yamlWriter.writeValueAsString(routingRule));
77+
updatedRoutingRulesBuilder.add(routingRule);
78+
}
79+
else {
80+
yamlContent.append(yamlWriter.writeValueAsString(rule));
81+
updatedRoutingRulesBuilder.add(rule);
82+
}
83+
}
84+
Files.writeString(path, yamlContent.toString(), UTF_8);
85+
lock.release();
86+
}
87+
catch (IOException e) {
88+
throw new UncheckedIOException("Failed to parse or update routing rules configuration form path : " + rulesConfigPath, e);
89+
}
90+
return updatedRoutingRulesBuilder.build();
91+
}
92+
}

0 commit comments

Comments
 (0)