diff --git a/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/CdsProtocolTransformer.java b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/CdsProtocolTransformer.java new file mode 100644 index 0000000000..490fb04ccb --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/CdsProtocolTransformer.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.implement.service.xds.utils; + +import io.envoyproxy.envoy.config.cluster.v3.Cluster; +import io.envoyproxy.envoy.config.cluster.v3.Cluster.LbPolicy; +import io.sermant.core.service.xds.entity.XdsCluster; +import io.sermant.core.service.xds.entity.XdsLbPolicy; +import io.sermant.core.service.xds.entity.XdsServiceCluster; +import io.sermant.core.utils.StringUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Convert cds protocol data to Sermant data model + * + * @author daizhenyu + * @since 2024-08-22 + **/ +public class CdsProtocolTransformer { + private static final int CLUSTER_SUBSET_INDEX = 2; + + private static final String VERTICAL_LINE_SEPARATOR = "\\|"; + + private static final Map LB_POLICY_MAPPING = new HashMap<>(); + + static { + LB_POLICY_MAPPING.put(LbPolicy.RANDOM, XdsLbPolicy.RANDOM); + LB_POLICY_MAPPING.put(LbPolicy.ROUND_ROBIN, XdsLbPolicy.ROUND_ROBIN); + LB_POLICY_MAPPING.put(LbPolicy.LEAST_REQUEST, XdsLbPolicy.LEAST_REQUEST); + LB_POLICY_MAPPING.put(LbPolicy.RING_HASH, XdsLbPolicy.RING_HASH); + LB_POLICY_MAPPING.put(LbPolicy.MAGLEV, XdsLbPolicy.MAGLEV); + } + + private CdsProtocolTransformer() { + } + + /** + * get the mapping between service name of k8s and cluster of istio + * + * @param clusters clusters + * @return XdsServiceCluster map + */ + public static Map getServiceClusters(List clusters) { + Map> xdsClusters = clusters.stream() + .filter(Objects::nonNull) + .map(cluster -> parseCluster(cluster)) + .filter(xdsCluster -> !StringUtils.isEmpty(xdsCluster.getServiceName())) + .collect(Collectors.groupingBy( + XdsCluster::getServiceName, + Collectors.toSet() + )); + Map xdsServiceClusterMap = new HashMap<>(); + for (Entry> clusterEntry : xdsClusters.entrySet()) { + XdsServiceCluster serviceCluster = new XdsServiceCluster(); + serviceCluster.setBaseClusterName(getServiceBaseClusterName(clusterEntry.getValue())); + Map clusterMap = clusterEntry.getValue().stream() + .collect(Collectors.toMap( + XdsCluster::getClusterName, + xdsCluster -> xdsCluster + )); + serviceCluster.setClusters(clusterMap); + xdsServiceClusterMap.put(clusterEntry.getKey(), serviceCluster); + } + return xdsServiceClusterMap; + } + + private static XdsCluster parseCluster(Cluster cluster) { + XdsCluster xdsCluster = new XdsCluster(); + Optional serviceNameFromCluster = XdsCommonUtils.getServiceNameFromCluster(cluster.getName()); + if (!serviceNameFromCluster.isPresent()) { + return xdsCluster; + } + xdsCluster.setClusterName(cluster.getName()); + xdsCluster.setServiceName(serviceNameFromCluster.get()); + xdsCluster.setLocalityLb(cluster.getCommonLbConfig().hasLocalityWeightedLbConfig()); + xdsCluster.setLbPolicy(parseClusterLbPolicy(cluster.getLbPolicy())); + return xdsCluster; + } + + private static String getServiceBaseClusterName(Set xdsClusters) { + for (XdsCluster cluster : xdsClusters) { + String clusterName = cluster.getClusterName(); + String[] splitCluster = clusterName.split(VERTICAL_LINE_SEPARATOR); + if (splitCluster[CLUSTER_SUBSET_INDEX].equals(StringUtils.EMPTY)) { + return clusterName; + } + } + return StringUtils.EMPTY; + } + + private static XdsLbPolicy parseClusterLbPolicy(LbPolicy lbPolicy) { + return LB_POLICY_MAPPING.getOrDefault(lbPolicy, XdsLbPolicy.UNRECOGNIZED); + } +} diff --git a/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/LdsProtocolTransformer.java b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/LdsProtocolTransformer.java new file mode 100644 index 0000000000..9ec8381a0d --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/LdsProtocolTransformer.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.implement.service.xds.utils; + +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; + +import io.envoyproxy.envoy.config.listener.v3.Filter; +import io.envoyproxy.envoy.config.listener.v3.Listener; +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; +import io.sermant.core.common.LoggerFactory; +import io.sermant.core.service.xds.entity.XdsHttpConnectionManager; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Convert lds protocol data to Sermant data model + * + * @author daizhenyu + * @since 2024-08-22 + **/ +public class LdsProtocolTransformer { + private static final Logger LOGGER = LoggerFactory.getLogger(); + + private LdsProtocolTransformer() { + } + + /** + * get HttpConnectionManager + * + * @param listeners listeners + * @return HttpConnectionManager list + */ + public static List getHttpConnectionManager(List listeners) { + return listeners.stream() + .filter(Objects::nonNull) + .flatMap(listener -> listener.getFilterChainsList().stream()) + .flatMap(e -> e.getFiltersList().stream()) + .map(Filter::getTypedConfig) + .map(LdsProtocolTransformer::unpackHttpConnectionManager) + .filter(Optional::isPresent) + .map(Optional::get) + .map(httpConnectionManager -> { + XdsHttpConnectionManager xdsHcm = new XdsHttpConnectionManager(); + xdsHcm.setRouteConfigName(httpConnectionManager.getRds().getRouteConfigName()); + return xdsHcm; + }) + .collect(Collectors.toList()); + } + + private static Optional unpackHttpConnectionManager(Any any) { + try { + if (!any.is(HttpConnectionManager.class)) { + return Optional.empty(); + } + return Optional.of(any.unpack(HttpConnectionManager.class)); + } catch (InvalidProtocolBufferException e) { + LOGGER.log(Level.SEVERE, "Decode resource to HttpConnectionManager failed.", e); + return Optional.empty(); + } + } +} diff --git a/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/RdsProtocolTransformer.java b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/RdsProtocolTransformer.java new file mode 100644 index 0000000000..c8448b2b8d --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/RdsProtocolTransformer.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.implement.service.xds.utils; + +import io.envoyproxy.envoy.config.route.v3.HeaderMatcher; +import io.envoyproxy.envoy.config.route.v3.Route; +import io.envoyproxy.envoy.config.route.v3.RouteAction; +import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; +import io.envoyproxy.envoy.config.route.v3.RouteMatch; +import io.envoyproxy.envoy.config.route.v3.VirtualHost; +import io.envoyproxy.envoy.config.route.v3.WeightedCluster; +import io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight; +import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; +import io.sermant.core.common.LoggerFactory; +import io.sermant.core.service.xds.entity.XdsHeaderMatcher; +import io.sermant.core.service.xds.entity.XdsPathMatcher; +import io.sermant.core.service.xds.entity.XdsRoute; +import io.sermant.core.service.xds.entity.XdsRouteAction; +import io.sermant.core.service.xds.entity.XdsRouteAction.XdsClusterWeight; +import io.sermant.core.service.xds.entity.XdsRouteAction.XdsWeightedClusters; +import io.sermant.core.service.xds.entity.XdsRouteConfiguration; +import io.sermant.core.service.xds.entity.XdsRouteMatch; +import io.sermant.core.service.xds.entity.XdsVirtualHost; +import io.sermant.core.service.xds.entity.match.ExactMatchStrategy; +import io.sermant.core.service.xds.entity.match.PrefixMatchStrategy; +import io.sermant.core.service.xds.entity.match.PresentMatchStrategy; +import io.sermant.core.service.xds.entity.match.RegexMatchStrategy; +import io.sermant.core.service.xds.entity.match.SuffixMatchStrategy; +import io.sermant.core.service.xds.entity.match.UnknownMatchStrategy; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Convert rds protocol data to Sermant data model + * + * @author daizhenyu + * @since 2024-08-22 + **/ +public class RdsProtocolTransformer { + private static final Logger LOGGER = LoggerFactory.getLogger(); + + private static final String POINT_SEPARATOR = "\\."; + + private RdsProtocolTransformer() { + } + + /** + * get XdsRouteConfiguration list + * + * @param routeConfigurations RouteConfiguration list + * @return XdsRouteConfiguration list + */ + public static List getRouteConfigurations(List routeConfigurations) { + return routeConfigurations.stream() + .filter(Objects::nonNull) + .map(routeConfiguration -> parseRouteConfiguration(routeConfiguration)) + .collect(Collectors.toList()); + } + + private static XdsRouteConfiguration parseRouteConfiguration(RouteConfiguration routeConfiguration) { + XdsRouteConfiguration xdsRouteConfiguration = new XdsRouteConfiguration(); + xdsRouteConfiguration.setRouteConfigName(routeConfiguration.getName()); + Map xdsVirtualHostMap = routeConfiguration.getVirtualHostsList().stream() + .map(virtualHost -> parseVirtualHost(virtualHost)) + .collect(Collectors.toMap( + xdsVirtualHost -> xdsVirtualHost.getName().split(POINT_SEPARATOR)[0], + xdsVirtualHost -> xdsVirtualHost + )); + xdsRouteConfiguration.setVirtualHosts(xdsVirtualHostMap); + return xdsRouteConfiguration; + } + + private static XdsVirtualHost parseVirtualHost(VirtualHost virtualHost) { + XdsVirtualHost xdsVirtualHost = new XdsVirtualHost(); + List domains = virtualHost.getDomainsList(); + List xdsRoutes = + virtualHost.getRoutesList().stream() + .map(route -> parseRoute(route)) + .collect(Collectors.toList()); + xdsVirtualHost.setName(virtualHost.getName()); + xdsVirtualHost.setRoutes(xdsRoutes); + xdsVirtualHost.setDomains(domains); + return xdsVirtualHost; + } + + private static XdsRoute parseRoute(Route route) { + XdsRoute xdsRoute = new XdsRoute(); + xdsRoute.setName(route.getName()); + xdsRoute.setRouteMatch(parseRouteMatch(route.getMatch())); + xdsRoute.setRouteAction(parseRouteAction(route.getRoute())); + return xdsRoute; + } + + private static XdsRouteMatch parseRouteMatch(RouteMatch routeMatch) { + XdsRouteMatch xdsRouteMatch = new XdsRouteMatch(); + xdsRouteMatch.setPathMatcher(parsePathMatcher(routeMatch)); + xdsRouteMatch.setHeaderMatchers(parseHeaderMatchers(routeMatch.getHeadersList())); + xdsRouteMatch.setCaseSensitive(routeMatch.getCaseSensitive().getValue()); + return xdsRouteMatch; + } + + private static List parseHeaderMatchers(List headerMatchers) { + return headerMatchers.stream() + .filter(Objects::nonNull) + .map(headerMatcher -> parseHeaderMatcher(headerMatcher)) + .collect(Collectors.toList()); + } + + private static XdsHeaderMatcher parseHeaderMatcher(HeaderMatcher headerMatcher) { + if (headerMatcher.getPresentMatch()) { + return new XdsHeaderMatcher(headerMatcher.getName(), + new PresentMatchStrategy()); + } + if (!headerMatcher.hasStringMatch()) { + LOGGER.log(Level.WARNING, + "The xDS route header matching strategy is unknown. Please check the route configuration."); + return new XdsHeaderMatcher(headerMatcher.getName(), + new UnknownMatchStrategy()); + } + StringMatcher stringMatch = headerMatcher.getStringMatch(); + switch (stringMatch.getMatchPatternCase()) { + case EXACT: + return new XdsHeaderMatcher(headerMatcher.getName(), + new ExactMatchStrategy(stringMatch.getExact())); + case PREFIX: + return new XdsHeaderMatcher(headerMatcher.getName(), + new PrefixMatchStrategy(stringMatch.getPrefix())); + case SUFFIX: + return new XdsHeaderMatcher(headerMatcher.getName(), + new SuffixMatchStrategy(stringMatch.getSuffix())); + case SAFE_REGEX: + return new XdsHeaderMatcher(headerMatcher.getName(), + new RegexMatchStrategy(stringMatch.getSafeRegex().getRegex())); + default: + LOGGER.log(Level.WARNING, + "The xDS route header matching strategy is unknown. Please check the route configuration."); + return new XdsHeaderMatcher(headerMatcher.getName(), + new UnknownMatchStrategy()); + } + } + + private static XdsPathMatcher parsePathMatcher(RouteMatch routeMatch) { + boolean caseSensitive = routeMatch.getCaseSensitive().getValue(); + switch (routeMatch.getPathSpecifierCase()) { + case PATH: + String path = routeMatch.getPath(); + return new XdsPathMatcher(new ExactMatchStrategy(caseSensitive ? path : path.toLowerCase(Locale.ROOT)), + caseSensitive); + case PREFIX: + String prefix = routeMatch.getPrefix(); + return new XdsPathMatcher( + new PrefixMatchStrategy(caseSensitive ? prefix : prefix.toLowerCase(Locale.ROOT)), + caseSensitive); + default: + LOGGER.log(Level.WARNING, + "The xDS route path matching strategy is unknown. Please check the route configuration."); + return new XdsPathMatcher(new UnknownMatchStrategy(), caseSensitive); + } + } + + private static XdsRouteAction parseRouteAction(RouteAction routeAction) { + XdsRouteAction xdsRouteAction = new XdsRouteAction(); + switch (routeAction.getClusterSpecifierCase()) { + case CLUSTER: + xdsRouteAction.setCluster(routeAction.getCluster()); + break; + case WEIGHTED_CLUSTERS: + xdsRouteAction.setWeighted(true); + xdsRouteAction.setWeightedClusters(parseWeightedClusters(routeAction.getWeightedClusters())); + break; + default: + LOGGER.log(Level.WARNING, + "The xDS route action strategy is unknown. Please check the route configuration."); + } + return xdsRouteAction; + } + + private static XdsWeightedClusters parseWeightedClusters(WeightedCluster clusters) { + XdsWeightedClusters xdsWeightedClusters = new XdsWeightedClusters(); + xdsWeightedClusters.setTotalWeight(clusters.getClustersList().stream() + .filter(Objects::nonNull) + .mapToInt(clusterWeight -> clusterWeight.getWeight().getValue()) + .sum() + ); + xdsWeightedClusters.setClusters(clusters.getClustersList().stream() + .filter(Objects::nonNull) + .map(clusterWeight -> parseClusterWeight(clusterWeight)) + .collect(Collectors.toList())); + return xdsWeightedClusters; + } + + private static XdsClusterWeight parseClusterWeight(ClusterWeight clusterWeight) { + XdsClusterWeight xdsClusterWeight = new XdsClusterWeight(); + xdsClusterWeight.setWeight(clusterWeight.getWeight().getValue()); + xdsClusterWeight.setClusterName(clusterWeight.getName()); + return xdsClusterWeight; + } +} diff --git a/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/XdsCommonUtils.java b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/XdsCommonUtils.java new file mode 100644 index 0000000000..df1d9b6666 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/xds/utils/XdsCommonUtils.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.implement.service.xds.utils; + +import io.sermant.core.utils.StringUtils; + +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * xds common utils + * + * @author daizhenyu + * @since 2024-08-22 + **/ +public class XdsCommonUtils { + private static final int SERVICE_HOST_INDEX = 3; + + private static final int SERVICE_NAME_INDEX = 0; + + private static final String VERTICAL_LINE_SEPARATOR = "\\|"; + + private static final String POINT_SEPARATOR = "\\."; + + private static final Pattern CLUSTER_NAME_FORMAT = Pattern.compile("^\\w+\\|\\w+\\|[^|]*\\|[^|]+$"); + + private XdsCommonUtils() { + } + + /*** + * get service name from cluster + * + * @param clusterName cluster name + * @return service name + */ + public static Optional getServiceNameFromCluster(String clusterName) { + if (StringUtils.isEmpty(clusterName) || !CLUSTER_NAME_FORMAT.matcher(clusterName).matches()) { + return Optional.empty(); + } + + // cluster name format: "outbound|8080||xds-service.default.svc.cluster.local", xds-service is service name + String[] clusterSplit = clusterName.split(VERTICAL_LINE_SEPARATOR); + return Optional.of(clusterSplit[SERVICE_HOST_INDEX].split(POINT_SEPARATOR)[SERVICE_NAME_INDEX]); + } +} diff --git a/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/CdsProtocolTransformerTest.java b/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/CdsProtocolTransformerTest.java new file mode 100644 index 0000000000..cd12830139 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/CdsProtocolTransformerTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.implement.service.xds.utils; + +import io.envoyproxy.envoy.config.cluster.v3.Cluster; +import io.envoyproxy.envoy.config.cluster.v3.Cluster.CommonLbConfig; +import io.envoyproxy.envoy.config.cluster.v3.Cluster.CommonLbConfig.Builder; +import io.envoyproxy.envoy.config.cluster.v3.Cluster.CommonLbConfig.LocalityWeightedLbConfig; +import io.envoyproxy.envoy.config.cluster.v3.Cluster.LbPolicy; +import io.sermant.core.service.xds.entity.XdsLbPolicy; +import io.sermant.core.service.xds.entity.XdsServiceCluster; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * CdsProtocolTransformerTest + * + * @author daizhenyu + * @since 2024-08-23 + **/ +public class CdsProtocolTransformerTest { + @Test + public void testGetServiceClusters() { + List clusters = Arrays.asList( + null, + createCluster("outbound|8080||serviceA.default.svc.cluster.local", LbPolicy.RANDOM, true), + createCluster("outbound|8080|subset1|serviceB.default.svc.cluster.local", LbPolicy.RANDOM, false), + createCluster("outbound|8080|subset2|serviceB.default.svc.cluster.local", LbPolicy.CLUSTER_PROVIDED, + false), + createCluster("outbound|8080||serviceB.default.svc.cluster.local", LbPolicy.RANDOM, false), + createCluster("outbound|8080|serviceC.default.svc.cluster.local", LbPolicy.RANDOM, false), + createCluster(null, null, false) + ); + + Map result = CdsProtocolTransformer.getServiceClusters(clusters); + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.containsKey("serviceA")); + Assert.assertTrue(result.containsKey("serviceB")); + XdsServiceCluster serviceACluster = result.get("serviceA"); + XdsServiceCluster serviceBCluster = result.get("serviceB"); + Assert.assertEquals(1, serviceACluster.getClusterResources().size()); + Assert.assertEquals("outbound|8080||serviceA.default.svc.cluster.local", + serviceACluster.getBaseClusterName()); + Assert.assertEquals(XdsLbPolicy.RANDOM, + serviceACluster.getLbPolicyOfCluster("outbound|8080||serviceA.default.svc.cluster.local")); + Assert.assertEquals(XdsLbPolicy.UNRECOGNIZED, + serviceACluster.getLbPolicyOfCluster("outbound|8080|aaa|serviceA.default.svc.cluster.local")); + Assert.assertEquals(XdsLbPolicy.RANDOM, + serviceACluster.getBaseLbPolicyOfService()); + Assert.assertEquals(true, + serviceACluster.isClusterLocalityLb("outbound|8080||serviceA.default.svc.cluster.local")); + + Assert.assertEquals(3, serviceBCluster.getClusterResources().size()); + Assert.assertEquals("outbound|8080||serviceA.default.svc.cluster.local", + result.get("serviceA").getBaseClusterName()); + Assert.assertEquals(XdsLbPolicy.RANDOM, + serviceBCluster.getLbPolicyOfCluster("outbound|8080||serviceB.default.svc.cluster.local")); + Assert.assertEquals(XdsLbPolicy.UNRECOGNIZED, + serviceBCluster.getLbPolicyOfCluster("outbound|8080|subset2|serviceB.default.svc.cluster.local")); + Assert.assertEquals(XdsLbPolicy.UNRECOGNIZED, + serviceBCluster.getLbPolicyOfCluster("outbound|8080|subset3|serviceB.default.svc.cluster.local")); + Assert.assertEquals(XdsLbPolicy.RANDOM, + serviceBCluster.getBaseLbPolicyOfService()); + Assert.assertEquals(false, + serviceACluster.isClusterLocalityLb("outbound|8080||serviceB.default.svc.cluster.local")); + } + + private Cluster createCluster(String name, LbPolicy lbPolicy, boolean isLocal) { + Cluster.Builder builder = Cluster.newBuilder(); + if (name != null) { + builder.setName(name); + } + if (lbPolicy != null) { + builder.setLbPolicy(lbPolicy); + } + if (isLocal) { + Builder configBuilder = CommonLbConfig.newBuilder(); + configBuilder.setLocalityWeightedLbConfig(LocalityWeightedLbConfig.newBuilder().build()); + builder.setCommonLbConfig(configBuilder.build()); + } + return builder.build(); + } +} \ No newline at end of file diff --git a/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/LdsProtocolTransformerTest.java b/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/LdsProtocolTransformerTest.java new file mode 100644 index 0000000000..e50422ee02 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/LdsProtocolTransformerTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.implement.service.xds.utils; + +import com.google.protobuf.Any; + +import io.envoyproxy.envoy.config.core.v3.Address; +import io.envoyproxy.envoy.config.core.v3.SocketAddress; +import io.envoyproxy.envoy.config.listener.v3.Filter; +import io.envoyproxy.envoy.config.listener.v3.FilterChain; +import io.envoyproxy.envoy.config.listener.v3.Listener; +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds; +import io.sermant.core.service.xds.entity.XdsHttpConnectionManager; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * LdsProtocolTransformerTest + * + * @author daizhenyu + * @since 2024-08-23 + **/ +public class LdsProtocolTransformerTest { + @Test + public void getHttpConnectionManager() { + List listeners = Arrays.asList( + null, + createListener("testA", "test-routeA"), + createListener("testB", "test-routeB"), + createListener("testC", null) + ); + List httpConnectionManagers = LdsProtocolTransformer + .getHttpConnectionManager(listeners); + Assert.assertEquals(2, httpConnectionManagers.size()); + Set actualRouteConfigNames = new HashSet<>(); + actualRouteConfigNames.add(httpConnectionManagers.get(0).getRouteConfigName()); + actualRouteConfigNames.add(httpConnectionManagers.get(1).getRouteConfigName()); + + Set expectRouteConfigNames = new HashSet<>(); + expectRouteConfigNames.add("test-routeA"); + expectRouteConfigNames.add("test-routeB"); + Assert.assertEquals(expectRouteConfigNames, actualRouteConfigNames); + } + + private Listener createListener(String listenerName, String routeName) { + Filter httpFilter; + if (routeName != null) { + HttpConnectionManager httpConnectionManager = createHttpConnectionManager(routeName); + Any httpConnectionManagerAny = Any.pack(httpConnectionManager); + httpFilter = Filter.newBuilder() + .setName("envoy.filters.network.http_connection_manager") + .setTypedConfig(httpConnectionManagerAny) + .build(); + } else { + httpFilter = Filter.newBuilder() + .build(); + } + FilterChain filterChain = FilterChain.newBuilder() + .addFilters(httpFilter) + .build(); + + Address listenerAddress = Address.newBuilder() + .setSocketAddress(SocketAddress.newBuilder() + .setAddress("127.0.0.1") + .setPortValue(8080) + .build()) + .build(); + + return Listener.newBuilder() + .setName(listenerName) + .setAddress(listenerAddress) + .addFilterChains(filterChain) + .build(); + } + + private HttpConnectionManager createHttpConnectionManager(String routeName) { + HttpConnectionManager.Builder managerBuilder = HttpConnectionManager.newBuilder(); + Rds.Builder rdsBuilder = Rds.newBuilder(); + rdsBuilder.setRouteConfigName(routeName); + managerBuilder.setRds(rdsBuilder.build()); + return managerBuilder.build(); + } +} \ No newline at end of file diff --git a/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/RdsProtocolTransformerTest.java b/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/RdsProtocolTransformerTest.java new file mode 100644 index 0000000000..75ec653e81 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/RdsProtocolTransformerTest.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.implement.service.xds.utils; + +import com.google.protobuf.BoolValue; +import com.google.protobuf.UInt32Value; + +import io.envoyproxy.envoy.config.route.v3.HeaderMatcher; +import io.envoyproxy.envoy.config.route.v3.Route; +import io.envoyproxy.envoy.config.route.v3.Route.Builder; +import io.envoyproxy.envoy.config.route.v3.RouteAction; +import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; +import io.envoyproxy.envoy.config.route.v3.RouteMatch; +import io.envoyproxy.envoy.config.route.v3.VirtualHost; +import io.envoyproxy.envoy.config.route.v3.WeightedCluster; +import io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight; +import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; +import io.sermant.core.service.xds.entity.XdsRoute; +import io.sermant.core.service.xds.entity.XdsRouteConfiguration; +import io.sermant.core.service.xds.entity.XdsVirtualHost; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * RdsProtocolTransformerTest + * + * @author daizhenyu + * @since 2024-08-24 + **/ +public class RdsProtocolTransformerTest { + private List routeConfigurations; + + @Test + public void testGetRouteConfigurationsWithPathTypeAndHeaderTypeAllExact() { + routeConfigurations = Arrays.asList( + createRouteConfiguration("exact", "exact") + ); + List result = RdsProtocolTransformer + .getRouteConfigurations(routeConfigurations); + Assert.assertEquals(1, result.size()); + + // pathType:exact headerType:exact + XdsRouteConfiguration routeConfiguration = result.get(0); + Assert.assertEquals("testRoute", routeConfiguration.getRouteConfigName()); + Map virtualHosts = routeConfiguration.getVirtualHosts(); + Assert.assertEquals(1, virtualHosts.size()); + Assert.assertTrue(virtualHosts.containsKey("serviceA")); + XdsVirtualHost virtualHost = virtualHosts.get("serviceA"); + List routes = virtualHost.getRoutes(); + Assert.assertEquals(2, routes.size()); + Set expectIsWeightedCluster = new HashSet<>(); + expectIsWeightedCluster.add(true); + expectIsWeightedCluster.add(false); + Set actualIsWeightedCluster = new HashSet<>(); + actualIsWeightedCluster.add(routes.get(0).getRouteAction().isWeighted()); + actualIsWeightedCluster.add(routes.get(1).getRouteAction().isWeighted()); + Assert.assertEquals(expectIsWeightedCluster, actualIsWeightedCluster); + Assert.assertTrue(routes.get(0).getRouteMatch().getPathMatcher().isMatch("/test")); + Map headers = new HashMap<>(); + headers.put("testHeader", "value"); + Assert.assertTrue(routes.get(0).getRouteMatch().getHeaderMatchers().get(0).isMatch(headers)); + } + + @Test + public void testGetRouteConfigurationsWithPathTypeAndHeaderTypeAllPrefix() { + List routeConfigurations = Arrays.asList( + createRouteConfiguration("prefix", "prefix") + ); + List result = RdsProtocolTransformer + .getRouteConfigurations(routeConfigurations); + Assert.assertEquals(1, result.size()); + + // pathType:prefix headerType:prefix + XdsRouteConfiguration routeConfiguration = result.get(0); + Assert.assertEquals("testRoute", routeConfiguration.getRouteConfigName()); + Map virtualHosts = routeConfiguration.getVirtualHosts(); + Assert.assertEquals(1, virtualHosts.size()); + Assert.assertTrue(virtualHosts.containsKey("serviceA")); + XdsVirtualHost virtualHost = virtualHosts.get("serviceA"); + List routes = virtualHost.getRoutes(); + Assert.assertEquals(2, routes.size()); + Set expectIsWeightedCluster = new HashSet<>(); + expectIsWeightedCluster.add(true); + expectIsWeightedCluster.add(false); + Set actualIsWeightedCluster = new HashSet<>(); + actualIsWeightedCluster.add(routes.get(0).getRouteAction().isWeighted()); + actualIsWeightedCluster.add(routes.get(1).getRouteAction().isWeighted()); + Assert.assertEquals(expectIsWeightedCluster, actualIsWeightedCluster); + Assert.assertTrue(routes.get(0).getRouteMatch().getPathMatcher().isMatch("/test/2")); + Map headers = new HashMap<>(); + headers.put("testHeader", "value-prefix"); + Assert.assertTrue(routes.get(0).getRouteMatch().getHeaderMatchers().get(0).isMatch(headers)); + } + + @Test + public void testGetRouteConfigurationsWithPathTypePrefixAndHeaderTypePresent() { + List routeConfigurations = Arrays.asList( + createRouteConfiguration("prefix", "present") + ); + List result = RdsProtocolTransformer + .getRouteConfigurations(routeConfigurations); + Assert.assertEquals(1, result.size()); + + // pathType:prefix headerType:present + XdsRouteConfiguration routeConfiguration = result.get(0); + Assert.assertEquals("testRoute", routeConfiguration.getRouteConfigName()); + Map virtualHosts = routeConfiguration.getVirtualHosts(); + Assert.assertEquals(1, virtualHosts.size()); + Assert.assertTrue(virtualHosts.containsKey("serviceA")); + XdsVirtualHost virtualHost = virtualHosts.get("serviceA"); + List routes = virtualHost.getRoutes(); + Assert.assertEquals(2, routes.size()); + Set expectIsWeightedCluster = new HashSet<>(); + expectIsWeightedCluster.add(true); + expectIsWeightedCluster.add(false); + Set actualIsWeightedCluster = new HashSet<>(); + actualIsWeightedCluster.add(routes.get(0).getRouteAction().isWeighted()); + actualIsWeightedCluster.add(routes.get(1).getRouteAction().isWeighted()); + Assert.assertEquals(expectIsWeightedCluster, actualIsWeightedCluster); + Assert.assertTrue(routes.get(0).getRouteMatch().getPathMatcher().isMatch("/test/3")); + Map headers = new HashMap<>(); + headers.put("testHeader", "present"); + Assert.assertTrue(routes.get(0).getRouteMatch().getHeaderMatchers().get(0).isMatch(headers)); + } + + private RouteConfiguration createRouteConfiguration(String pathType, String headerType) { + return RouteConfiguration.newBuilder() + .setName("testRoute") + .addVirtualHosts(createVirtualHost(pathType, headerType)) + .build(); + } + + private VirtualHost createVirtualHost(String pathType, String headerType) { + return VirtualHost.newBuilder() + .setName("serviceA.example.com") + .addDomains("serviceA.example.com") + .addRoutes(createRoute(true, pathType, headerType)) + .addRoutes(createRoute(false, pathType, headerType)) + .build(); + } + + private Route createRoute(boolean isWeightCluster, String pathType, String headerType) { + Builder routeBuilder = Route.newBuilder(); + if (isWeightCluster) { + routeBuilder.setRoute(createRouteActionWithWeightCluster()); + } else { + routeBuilder.setRoute(createRouteAction()); + } + return routeBuilder.setMatch(createRouteMatch(pathType, headerType)) + .build(); + } + + private RouteMatch createRouteMatch(String pathType, String headerType) { + RouteMatch.Builder matchBuilder = RouteMatch.newBuilder(); + if ("exact".equals(pathType)) { + matchBuilder.setPath("/test"); + } else if ("prefix".equals(pathType)) { + matchBuilder.setPrefix("/test"); + } + return matchBuilder + .setCaseSensitive(BoolValue.of(true)) + .addHeaders(createHeaderMatcher(headerType)) + .build(); + } + + private RouteAction createRouteAction() { + return RouteAction.newBuilder() + .setCluster("testCluster") + .build(); + } + + private RouteAction createRouteActionWithWeightCluster() { + return RouteAction.newBuilder() + .setWeightedClusters(createWeightedCluster()) + .build(); + } + + private HeaderMatcher createHeaderMatcher(String headerType) { + HeaderMatcher.Builder headBuilder = HeaderMatcher.newBuilder().setName("testHeader"); + StringMatcher.Builder stringMatcherBuilder = StringMatcher.newBuilder(); + switch (headerType) { + case "exact": + headBuilder.setStringMatch(stringMatcherBuilder.setExact("value")); + break; + case "prefix": + headBuilder.setStringMatch(stringMatcherBuilder.setPrefix("value")); + break; + case "present": + headBuilder.setPresentMatch(true); + break; + } + return headBuilder.build(); + } + + private WeightedCluster createWeightedCluster() { + return WeightedCluster.newBuilder() + .setTotalWeight(UInt32Value.of(100)) + .addClusters(createClusterWeight()) + .build(); + } + + private ClusterWeight createClusterWeight() { + return ClusterWeight.newBuilder() + .setName("clusterName") + .setWeight(UInt32Value.of(100)) + .build(); + } +} \ No newline at end of file diff --git a/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/XdsCommonUtilsTest.java b/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/XdsCommonUtilsTest.java new file mode 100644 index 0000000000..6c045e9efb --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-implement/src/test/java/io/sermant/implement/service/xds/utils/XdsCommonUtilsTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.implement.service.xds.utils; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Optional; + +/** + * XdsCommonUtilsTest + * + * @author daizhenyu + * @since 2024-08-23 + **/ +public class XdsCommonUtilsTest { + @Test + public void testGetServiceNameFromCluster() { + String clusterName = "outbound|8080||serviceA.default.svc.cluster.local"; + Optional serviceNameFromCluster = XdsCommonUtils.getServiceNameFromCluster(clusterName); + Assert.assertTrue(serviceNameFromCluster.isPresent()); + Assert.assertEquals("serviceA", serviceNameFromCluster.get()); + + clusterName = "outbound|8080|sub-set|serviceA.default.svc.cluster.local"; + serviceNameFromCluster = XdsCommonUtils.getServiceNameFromCluster(clusterName); + Assert.assertTrue(serviceNameFromCluster.isPresent()); + Assert.assertEquals("serviceA", serviceNameFromCluster.get()); + + clusterName = "outbound|8080|subset|serviceA"; + serviceNameFromCluster = XdsCommonUtils.getServiceNameFromCluster(clusterName); + Assert.assertTrue(serviceNameFromCluster.isPresent()); + Assert.assertEquals("serviceA", serviceNameFromCluster.get()); + + clusterName = "outbound|8080|serviceA.default.svc.cluster.local"; + serviceNameFromCluster = XdsCommonUtils.getServiceNameFromCluster(clusterName); + Assert.assertFalse(serviceNameFromCluster.isPresent()); + } +} \ No newline at end of file