Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show and modify routing rules from the UI #433

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
33 changes: 33 additions & 0 deletions docs/gateway-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,36 @@ Will return a JSON array of active Trino cluster backends:
curl -X POST http://localhost:8080/gateway/backend/activate/trino-2
```

## Update Routing Rules
prakhar10 marked this conversation as resolved.
Show resolved Hide resolved

This API can be used to programmatically update the Routing Rules.
Rule will be updated based on the rule name.

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.

```shell
curl -X POST http://localhost:8080/webapp/updateRoutingRules \
-d '{ "name": "trino-rule",
"description": "updated rule description",
"priority": 0,
"actions": ["updated action"],
"condition": "updated condition"
}'
```
### Disable Routing Rules UI

You can set the `disablePages` config to disable pages on the UI.

The following pages are available:
- `dashboard`
- `cluster`
- `resource-group`
- `selector`
- `history`
- `routing-rules`

```yaml
uiConfiguration:
disablePages:
- 'routing-rules'
```
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.trino.gateway.ha.resource.LoginResource;
import io.trino.gateway.ha.resource.PublicResource;
import io.trino.gateway.ha.resource.TrinoResource;
import io.trino.gateway.ha.router.RoutingRulesManager;
import io.trino.gateway.ha.security.AuthorizedExceptionMapper;
import io.trino.gateway.proxyserver.ForProxy;
import io.trino.gateway.proxyserver.ProxyRequestHandler;
Expand Down Expand Up @@ -125,6 +126,7 @@ public void configure(Binder binder)
jaxrsBinder(binder).bind(AuthorizedExceptionMapper.class);
binder.bind(ProxyHandlerStats.class).in(Scopes.SINGLETON);
newExporter(binder).export(ProxyHandlerStats.class).withGeneratedName();
binder.bind(RoutingRulesManager.class);
}

private static void addManagedApps(HaGatewayConfiguration configuration, Binder binder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public class HaGatewayConfiguration

private RequestAnalyzerConfig requestAnalyzerConfig = new RequestAnalyzerConfig();

private UIConfiguration uiConfiguration = new UIConfiguration();

// List of Modules with FQCN (Fully Qualified Class Name)
private List<String> modules;

Expand Down Expand Up @@ -213,6 +215,16 @@ public void setRequestAnalyzerConfig(RequestAnalyzerConfig requestAnalyzerConfig
this.requestAnalyzerConfig = requestAnalyzerConfig;
}

public UIConfiguration getUiConfiguration()
{
return uiConfiguration;
}

public void setUiConfiguration(UIConfiguration uiConfiguration)
{
this.uiConfiguration = uiConfiguration;
}

public List<String> getModules()
{
return this.modules;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.gateway.ha.config;

import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.List;

public class UIConfiguration
{
private List<String> disablePages;

@JsonProperty
public List<String> getDisablePages()
{
return disablePages;
}

public void setDisablePages(List<String> disablePages)
{
this.disablePages = disablePages;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.gateway.ha.domain;

import com.google.common.collect.ImmutableList;
import jakarta.annotation.Nullable;

import java.util.List;

import static java.util.Objects.requireNonNull;

/**
* RoutingRules
*
* @param name name of the routing rule
* @param description description of the routing rule
* @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.
* @param actions actions of the routing rule
* @param condition condition of the routing rule
*/
public record RoutingRule(
String name,
@Nullable String description,
@Nullable Integer priority,
List<String> actions,
String condition)
{
public RoutingRule {
requireNonNull(name, "name is null");
actions = ImmutableList.copyOf(actions);
requireNonNull(condition, "condition is null");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
import com.google.common.base.Strings;
import com.google.inject.Inject;
import io.trino.gateway.ha.clustermonitor.ClusterStats;
import io.trino.gateway.ha.config.HaGatewayConfiguration;
import io.trino.gateway.ha.config.ProxyBackendConfiguration;
import io.trino.gateway.ha.config.RoutingRulesConfiguration;
import io.trino.gateway.ha.config.UIConfiguration;
import io.trino.gateway.ha.domain.Result;
import io.trino.gateway.ha.domain.RoutingRule;
import io.trino.gateway.ha.domain.TableData;
import io.trino.gateway.ha.domain.request.GlobalPropertyRequest;
import io.trino.gateway.ha.domain.request.QueryDistributionRequest;
Expand All @@ -34,8 +38,10 @@
import io.trino.gateway.ha.router.HaGatewayManager;
import io.trino.gateway.ha.router.QueryHistoryManager;
import io.trino.gateway.ha.router.ResourceGroupsManager;
import io.trino.gateway.ha.router.RoutingRulesManager;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
Expand All @@ -44,6 +50,7 @@
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
Expand All @@ -67,18 +74,26 @@ public class GatewayWebAppResource
private final QueryHistoryManager queryHistoryManager;
private final BackendStateManager backendStateManager;
private final ResourceGroupsManager resourceGroupsManager;
private final RoutingRulesConfiguration routingRulesConfiguration;
private final UIConfiguration uiConfiguration;
private final RoutingRulesManager routingRulesManager;

@Inject
public GatewayWebAppResource(
GatewayBackendManager gatewayBackendManager,
QueryHistoryManager queryHistoryManager,
BackendStateManager backendStateManager,
ResourceGroupsManager resourceGroupsManager)
ResourceGroupsManager resourceGroupsManager,
HaGatewayConfiguration configuration,
RoutingRulesManager routingRulesManager)
{
this.gatewayBackendManager = requireNonNull(gatewayBackendManager, "gatewayBackendManager is null");
this.queryHistoryManager = requireNonNull(queryHistoryManager, "queryHistoryManager is null");
this.backendStateManager = requireNonNull(backendStateManager, "backendStateManager is null");
this.resourceGroupsManager = requireNonNull(resourceGroupsManager, "resourceGroupsManager is null");
this.routingRulesConfiguration = configuration.getRoutingRules();
this.uiConfiguration = configuration.getUiConfiguration();
this.routingRulesManager = requireNonNull(routingRulesManager, "routingRulesManager is null");
}

@POST
Expand Down Expand Up @@ -424,4 +439,36 @@ public Response readExactMatchSourceSelector()
List<ResourceGroupsManager.ExactSelectorsDetail> selectorsDetailList = resourceGroupsManager.readExactMatchSourceSelector();
return Response.ok(Result.ok(selectorsDetailList)).build();
}

@GET
ebyhr marked this conversation as resolved.
Show resolved Hide resolved
@RolesAllowed("USER")
@Produces(MediaType.APPLICATION_JSON)
@Path("/getRoutingRules")
Chaho12 marked this conversation as resolved.
Show resolved Hide resolved
public Response getRoutingRules()
throws IOException
{
List<RoutingRule> routingRulesList = routingRulesManager.getRoutingRules(routingRulesConfiguration);
return Response.ok(Result.ok(routingRulesList)).build();
}

@POST
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
willmostly marked this conversation as resolved.
Show resolved Hide resolved
@Produces(MediaType.APPLICATION_JSON)
@Path("/updateRoutingRules")
public synchronized Response updateRoutingRules(RoutingRule routingRules)
throws IOException
{
List<RoutingRule> routingRulesList = routingRulesManager.updateRoutingRules(routingRules, routingRulesConfiguration);
return Response.ok(Result.ok(routingRulesList)).build();
}

@GET
@RolesAllowed("USER")
@Produces(MediaType.APPLICATION_JSON)
@Path("/getUIConfiguration")
public Response getUIConfiguration()
{
return Response.ok(Result.ok(uiConfiguration)).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.gateway.ha.router;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
import com.google.common.collect.ImmutableList;
import io.trino.gateway.ha.config.RoutingRulesConfiguration;
import io.trino.gateway.ha.domain.RoutingRule;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import static java.nio.charset.StandardCharsets.UTF_8;

public class RoutingRulesManager
prakhar10 marked this conversation as resolved.
Show resolved Hide resolved
{
public List<RoutingRule> getRoutingRules(RoutingRulesConfiguration configuration)
throws IOException
{
ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
String rulesConfigPath = configuration.getRulesConfigPath();
try {
String content = Files.readString(Paths.get(rulesConfigPath), UTF_8);
YAMLParser parser = new YAMLFactory().createParser(content);
List<RoutingRule> routingRulesList = new ArrayList<>();
prakhar10 marked this conversation as resolved.
Show resolved Hide resolved
while (parser.nextToken() != null) {
RoutingRule routingRules = yamlReader.readValue(parser, RoutingRule.class);
prakhar10 marked this conversation as resolved.
Show resolved Hide resolved
routingRulesList.add(routingRules);
}
return routingRulesList;
}
catch (IOException e) {
throw new IOException("Failed to read or parse routing rules configuration from path : " + rulesConfigPath, e);
}
}

public List<RoutingRule> updateRoutingRules(RoutingRule routingRules, RoutingRulesConfiguration configuration)
prakhar10 marked this conversation as resolved.
Show resolved Hide resolved
throws IOException
{
ImmutableList.Builder<RoutingRule> routingRulesBuilder = ImmutableList.builder();
String rulesConfigPath = configuration.getRulesConfigPath();
try {
List<RoutingRule> routingRulesList = getRoutingRules(configuration);
prakhar10 marked this conversation as resolved.
Show resolved Hide resolved
for (int i = 0; i < routingRulesList.size(); i++) {
if (routingRulesList.get(i).name().equals(routingRules.name())) {
routingRulesList.set(i, routingRules);
break;
}
}
ObjectMapper yamlWriter = new ObjectMapper(new YAMLFactory());
StringBuilder yamlContent = new StringBuilder();
for (RoutingRule rule : routingRulesList) {
yamlContent.append(yamlWriter.writeValueAsString(rule));
routingRulesBuilder.add(rule);
}
Files.writeString(Paths.get(rulesConfigPath), yamlContent.toString(), UTF_8);
}
catch (IOException e) {
throw new IOException("Failed to parse or update routing rules configuration form path : " + rulesConfigPath, e);
}
return routingRulesBuilder.build();
}
}
Loading