spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.52k stars 3.32k forks source link

Include route map configuration so routes across property sources can be merged #831

Open madhugopinath opened 5 years ago

madhugopinath commented 5 years ago

In zuul, we were able to configure routes across multiple files since its read as a map (with the id as key). But in spring cloud gateway, since routes are read as list, this is not possible. Any workaround or plans to enhance this?

ryanjbaxter commented 5 years ago

Are you doing something like configuring routes in different configuration files and then enabling them via profiles? You can specify ids for routes using a gateway.

madhugopinath commented 5 years ago

That's right. I would want to include multiple profiles at a time.

spencergibb commented 5 years ago

We don't do anything specific here. It's all spring boot external configuration. I believe this is a restriction in boot 2.x. What version of spring cloud were you using to do this in zuul?

madhugopinath commented 5 years ago

Yes, it is the behaviour of spring boot property binding. Lists cannot be merged from multiple files, but maps can be. ZuulProperties load routes into a map. But here it's a list and that's creating this limitation.

https://github.com/spring-cloud/spring-cloud-netflix/blob/master/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java

private Map<String, ZuulRoute> routes = new LinkedHashMap<>();

https://github.com/spring-cloud/spring-cloud-gateway/blob/master/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java

private List routes = new ArrayList<>();

spencergibb commented 5 years ago

So, we'd have to do this in a backwards compatible way. The map has to maintain order. Probably a new field, routes-map or something and validation that you have one or the other. The list would get put into the map with the id as the key.

ronaldewatts commented 4 years ago

Has there been any progress on this or is there any way I can help? Having the ability to load multiple files is valuable when trying to organize many routes. I'm also wondering if this feature would have the ability to merge default routes (the built-in application.yml) with a configuration coming from a Spring Cloud Config server?

daubhatt commented 3 years ago

Try creating a @Configuration class like below and define routes in mentioned format. Also, add a PostConstruct in AppConfig. Map being able to be merged using multiple properties/ yml files, solves this issue.

routes:
  example-service:
    uri: http://localhost:10002
    predicates:
      - Path=/example-service/hello
    filters:
      - CustomFilter=uri, http://localhost:10002
      - AddRequestHeader=X-Request-red, blue
      - AddRequestParameter=red, blue
@Configuration
@ConfigurationProperties("spring.cloud.gateway")
@RequiredArgsConstructor
@Slf4j
public class CustomRouteConfig {

    private final RouteDefinitionWriter writer;
    private Map<String, RouteDefinition> routes;

    public void setRoutes(Map<String, RouteDefinition> routes) {
        this.routes = routes;
    }

    // This is loading routes dynamically reading from routes map rather spring routes list
    @PostConstruct
    public void init() {
        this.routes.forEach((key, routeDef) -> this.writer.save(Mono.just(routeDef).map(route -> {
            route.setId(key);
            log.info("Saving route: " + route);
            return route;
        })).subscribe());
    }
}
skorhone commented 3 years ago

Another way of implementing this feature without modifying spring cloud gateway would require creating a custom EnvironmentPostProcessor that simply translates a map to list

spencergibb commented 3 years ago

Indeed. That would be ideal since it doesn't break compatibility.

Rohit-ahuja commented 3 years ago

Another way of implementing this feature without modifying spring cloud gateway would require creating a custom EnvironmentPostProcessor that simply translates a map to list

@skorhone Can you please share some sample code on how to do this

elmuerte commented 3 years ago

I've used a slightly different way to achieve the same result by using a custom RouteDefinitionLocator

@ConfigurationProperties("my-gateway")
public class MyGatewayProperties {
    private Map<String, RouteDefinition> routes = new LinkedHashMap<>();

    public Map<String, RouteDefinition> getRoutes() {
        return routes;
    }
}

@Component
@EnableConfigurationProperties(MyGatewayProperties.class)
public class MapPropertiesRouteDefinitionLocator implements RouteDefinitionLocator {
    private final MyGatewayProperties properties;

    public MapPropertiesRouteDefinitionLocator(MyGatewayProperties properties) {
        this.properties = properties;
    }

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        return Flux.fromIterable(properties.getRoutes().entrySet()).map(this::processEntry);
    }

    private RouteDefinition processEntry(Entry<String, RouteDefinition> entry) {
        final RouteDefinition route = entry.getValue();
        // ensure the route has an ID.
        if (route.getId() == null) {
            route.setId(entry.getKey());
        }
        return route;
    }
}
jacob2221 commented 3 years ago

As part of spring boot 2.4 (https://spring.io/blog/2020/08/14/config-file-processing-in-spring-boot-2-4) a new property "spring.config.import" has been added to import properties from multiple files. Will this help in achieving this requirement?

akcodian commented 2 years ago

wondering if there was any update after this. I tried both solutions but they don't seems to work. I have 4 Yamls that gateway pulls them from config server. each yaml has routes specific to a functional module. Routes map contains route Ids from the last yaml only

epiard13 commented 1 year ago

Hi @spencergibb @ryanjbaxter, Hope ya'll are doing well... Is there anything in the works for this ☝️ please ? Seems pretty important, otherwise all the routes have to be declared in one yaml file, which is a bit of a headache to manage over time as number of routes grow. Thanks

bemygreenheart commented 1 year ago

Any news on this, It is strange that such an important issue has been open for so long

daubhatt commented 1 year ago

@spencergibb added the fix for config to SCG

48:30 https://bootifulpodcast.podbean.com/e/spring-cloud-cofounder-and-lead-spencer-gibb-on-spring-cloud-gateway-for-the-servlet-api-in-the-era-of-project-loom/

spencergibb commented 1 year ago

@daubhatt currently only in the mvc version that will be released later this year.

dashdhirens commented 1 year ago

I was able to workaround this by defining a custom property dash.routes.overrides in my environment specific .yml file which contains a list of routes. These routes will override the actual routes (which can be defined in the main .yml file). I then create a bean of type PropertiesRouteDefinitionLocator with the final list of routes.

dash-api-gateway.yml

spring:
  cloud:
    gateway:
      routes:
        - id: test-route
           uri: http://dash-test:8080
           predicates:
             - Path=/dash-test/route
           filters
             - RewritePath=/(?<segment>.*), /dash-route/${spring.profiles.active}/isro/chandrayaan.json

dash-api-gateway-dev.yml

dash:
  routes:
    overrides:
      - id: test-route
         uri: http://dash-test-overridden:8080
         predicates:
           - Path=/dash-test/route
         filters
           - RewritePath=/(?<segment>.*), /dash-route/${spring.profiles.active}/isro/overridden.json

The ids of both of them need to be same.

RoutesOverrides.class - This will read all the overridden routes into a Map.

@Configuration
@ConfigurationProperties("dash.routes")
@RequiredArgsConstructor
public class RouteOverrides {

    private List<RouteDefinition> overrides;
    private Map<String, RouteDefinition> overridesMap = new HashMap<>();

    public void setOverrides(List<RouteDefinition> overrides) { this.overrides = overrides; }

    public Map<String, RouteDefinition> getOverridesMap() {
        return Collections.unmodifiableMap(overridesMap);
    }

    @PostConstruct
    public void init() {
        if (CollectionUtils.isEmpty(overrides)) {
            this.overridesMap = Collections.emptyMap();
        } else {
            // convert list of overrides to Map containing key as routeId and value as the route.
        }
    }
}

Bean creation code -

@Bean
public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(GatewayProperties properties) {

    // inject RouteOverrides bean into this config class and get overrides map
    Map<String, RouteDefinition> overridesMap = routeOverrides.getOverridesMap();
    Map<String, RouteDefinition> mapExistingRoutesById = properties.getRoutes.stream()
            .collect(Collectors.toMap(RouteDefinition::getId, Function.identity()));

    Map<String, RouteDefinition> finalRoutesMap = new HashMap<>();

    finalRoutesMap.putAll(mapExistingRoutesById);
    finalRoutesMap.putAll(overridesMap);

    return new PropertiesRouteDefinitionLocator(finalRoutesMap.values().stream().toList());
}
bemygreenheart commented 1 year ago

@spencergibb could we check the future release plans, and know in which version it will be released and when?

chiangzi commented 9 months ago

@akcodian you should add ID as map entry before routes list in YAML files, eg: spring.cloud.gateway.routes.ID.[List]