httpRouteWorkQueue,
+ final ApiClient apiClient) {
+ this.gatewayLister = new Lister<>(gatewayInformer.getIndexer());
+ this.httpRouteLister = new Lister<>(httpRouteInformer.getIndexer());
+ this.shenyuCacheRepository = shenyuCacheRepository;
+ this.httpRouteWorkQueue = httpRouteWorkQueue;
+ this.apiClient = apiClient;
+ }
+
+ @Override
+ public Result reconcile(final Request request) {
+ LOG.info("Starting to reconcile gateway {}", request);
+ try {
+ DynamicKubernetesObject gateway = gatewayLister.namespace(request.getNamespace()).get(request.getName());
+
+ if (Objects.isNull(gateway)) {
+ LOG.info("Gateway {} deleted, cleaning associated routes", request);
+ deleteAssociatedRoutes(request.getNamespace(), request.getName());
+ return new Result(false);
+ }
+
+ if (!isShenyuGateway(gateway)) {
+ LOG.info("Gateway {} is not managed by ShenYu, skipping", request);
+ return new Result(false);
+ }
+
+ updateGatewayAcceptedStatus(gateway);
+
+ // Re-queue HTTPRoutes that reference this Gateway but haven't been applied yet
+ requeueAffectedHTTPRoutes(request.getNamespace(), request.getName());
+
+ LOG.info("Gateway {} reconciled successfully", request);
+ return new Result(false);
+ } catch (Exception e) {
+ LOG.error("Error reconciling gateway {}, will retry", request, e);
+ return new Result(true);
+ }
+ }
+
+ /**
+ * When a ShenYu Gateway is created/updated, find HTTPRoutes whose parentRefs reference
+ * this Gateway and add them to the HTTPRoute controller's work queue for re-reconciliation.
+ * This handles the case where an HTTPRoute was created before the Gateway existed.
+ * Also handles cross-namespace references where HTTPRoute's parentRef specifies a different namespace.
+ *
+ * Uses GatewayRouteCache as primary lookup for already-bound routes to avoid full-cluster scans
+ * in large deployments. Falls back to informer scanning only for routes not yet tracked in cache
+ * (e.g., when a Gateway is first created and no HTTPRoute has been successfully reconciled yet).
+ */
+ private void requeueAffectedHTTPRoutes(final String gatewayNamespace, final String gatewayName) {
+ GatewayRouteCache cache = GatewayRouteCache.getInstance();
+ List cachedRoutes = cache.getRoutesByGateway(gatewayNamespace, gatewayName);
+
+ if (CollectionUtils.isNotEmpty(cachedRoutes)) {
+ // Fast path: routes already bound in cache, re-queue by parsed keys
+ for (String routeKey : cachedRoutes) {
+ String[] parts = routeKey.split("/", 2);
+ if (parts.length >= 2) {
+ Request req = new Request(parts[0], parts[1]);
+ httpRouteWorkQueue.add(req);
+ LOG.info("Re-queued cached HTTPRoute {}/{} due to Gateway {}/{} reconciliation",
+ parts[0], parts[1], gatewayNamespace, gatewayName);
+ }
+ }
+ // Also scan for cross-namespace routes that may not be in cache yet
+ for (DynamicKubernetesObject route : httpRouteLister.list()) {
+ String routeNamespace = Objects.requireNonNull(route.getMetadata()).getNamespace();
+ if (routeNamespace.equals(gatewayNamespace)) {
+ continue;
+ }
+ if (isBoundToGateway(route, gatewayNamespace, gatewayName)) {
+ Request req = new Request(route.getMetadata().getNamespace(), route.getMetadata().getName());
+ httpRouteWorkQueue.add(req);
+ LOG.info("Re-queued cross-namespace HTTPRoute {}/{} due to Gateway {}/{} reconciliation",
+ route.getMetadata().getNamespace(), route.getMetadata().getName(),
+ gatewayNamespace, gatewayName);
+ }
+ }
+ return;
+ }
+
+ // Cache miss: Gateway just created, no routes reconciled yet. Fall back to scanning.
+ List localRoutes = httpRouteLister.namespace(gatewayNamespace).list();
+ for (DynamicKubernetesObject route : localRoutes) {
+ if (isBoundToGateway(route, gatewayNamespace, gatewayName)) {
+ Request req = new Request(route.getMetadata().getNamespace(), route.getMetadata().getName());
+ httpRouteWorkQueue.add(req);
+ LOG.info("Re-queued HTTPRoute {}/{} due to Gateway {}/{} reconciliation",
+ route.getMetadata().getNamespace(), route.getMetadata().getName(),
+ gatewayNamespace, gatewayName);
+ }
+ }
+ for (DynamicKubernetesObject route : httpRouteLister.list()) {
+ String routeNamespace = Objects.requireNonNull(route.getMetadata()).getNamespace();
+ if (routeNamespace.equals(gatewayNamespace)) {
+ // Already handled in local routes search above
+ continue;
+ }
+ if (isBoundToGateway(route, gatewayNamespace, gatewayName)) {
+ Request req = new Request(route.getMetadata().getNamespace(), route.getMetadata().getName());
+ httpRouteWorkQueue.add(req);
+ LOG.info("Re-queued cross-namespace HTTPRoute {}/{} due to Gateway {}/{} reconciliation",
+ route.getMetadata().getNamespace(), route.getMetadata().getName(),
+ gatewayNamespace, gatewayName);
+ }
+ }
+ }
+
+ private boolean isBoundToGateway(final DynamicKubernetesObject httpRoute,
+ final String gatewayNamespace, final String gatewayName) {
+ JsonObject spec = httpRoute.getRaw().getAsJsonObject("spec");
+ if (Objects.isNull(spec) || !spec.has("parentRefs")) {
+ return false;
+ }
+ JsonArray parentRefs = spec.getAsJsonArray("parentRefs");
+ if (Objects.isNull(parentRefs)) {
+ return false;
+ }
+ String routeNamespace = Objects.requireNonNull(httpRoute.getMetadata()).getNamespace();
+ for (JsonElement element : parentRefs) {
+ JsonObject parentRef = element.getAsJsonObject();
+ String parentName = parentRef.has("name") ? parentRef.get("name").getAsString() : null;
+ String parentNamespace = parentRef.has("namespace") ? parentRef.get("namespace").getAsString() : routeNamespace;
+ if (gatewayNamespace.equals(parentNamespace) && gatewayName.equals(parentName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * When a Gateway is deleted, cascade delete ShenYu config (selectors/rules) for all associated routes.
+ */
+ private void deleteAssociatedRoutes(final String gatewayNamespace, final String gatewayName) {
+ GatewayRouteCache cache = GatewayRouteCache.getInstance();
+ List routeKeys = cache.getRoutesByGateway(gatewayNamespace, gatewayName);
+ if (CollectionUtils.isEmpty(routeKeys)) {
+ return;
+ }
+ // Copy to avoid ConcurrentModificationException: removeRouteGatewayBinding modifies the same list
+ List routeKeysCopy = new ArrayList<>(routeKeys);
+ // Remove gateway-route bindings first to prevent concurrent access issues
+ cache.removeRoutesByGateway(gatewayNamespace, gatewayName);
+ for (String routeKey : routeKeysCopy) {
+ String[] parts = routeKey.split("/", 2);
+ if (parts.length != 2) {
+ continue;
+ }
+ String routeNamespace = parts[0];
+ String routeName = parts[1];
+ List selectorIds = cache.removeRouteSelectors(routeNamespace, routeName, PluginEnum.DIVIDE.getName());
+ if (CollectionUtils.isNotEmpty(selectorIds)) {
+ for (String selectorId : selectorIds) {
+ List rules = shenyuCacheRepository.findRuleDataList(selectorId);
+ if (CollectionUtils.isNotEmpty(rules)) {
+ for (RuleData rule : rules) {
+ shenyuCacheRepository.deleteRuleData(PluginEnum.DIVIDE.getName(), selectorId, rule.getId());
+ }
+ }
+ shenyuCacheRepository.deleteSelectorData(PluginEnum.DIVIDE.getName(), selectorId);
+ }
+ }
+ LOG.info("Deleted ShenYu config for route {}/{} due to Gateway deletion", routeNamespace, routeName);
+ }
+ }
+
+ /**
+ * Check if the given Gateway object is managed by ShenYu.
+ *
+ * @param gateway the Gateway dynamic object
+ * @return true if the Gateway's gatewayClassName matches ShenYu
+ */
+ public static boolean isShenyuGateway(final DynamicKubernetesObject gateway) {
+ JsonObject spec = gateway.getRaw().getAsJsonObject("spec");
+ if (Objects.isNull(spec)) {
+ return false;
+ }
+ if (!spec.has("gatewayClassName") || spec.get("gatewayClassName").isJsonNull()) {
+ return false;
+ }
+ String gatewayClassName = spec.get("gatewayClassName").getAsString();
+ return GatewayApiConstants.SHENYU_GATEWAY_CLASS_NAME.equals(gatewayClassName);
+ }
+
+ /**
+ * Update Gateway status with Accepted=True condition.
+ * Uses strategic merge patch on the /status subresource to avoid Gson JsonElement serialization issues
+ * with the default updateStatus implementation.
+ */
+ private void updateGatewayAcceptedStatus(final DynamicKubernetesObject gateway) {
+ if (GatewayApiConstants.isConditionTrue(gateway, "Accepted")) {
+ return;
+ }
+ try {
+ final String namespace = gateway.getMetadata().getNamespace();
+ final String name = gateway.getMetadata().getName();
+
+ JsonObject condition = new JsonObject();
+ condition.addProperty("type", "Accepted");
+ condition.addProperty("status", "True");
+ condition.addProperty("reason", "Accepted");
+ condition.addProperty("message", "Gateway has been accepted by the ShenYu controller");
+ condition.addProperty("lastTransitionTime", Instant.now().toString());
+
+ JsonArray conditions = buildGatewayStatusConditions(gateway, condition);
+
+ JsonObject statusObj = new JsonObject();
+ statusObj.add("conditions", conditions);
+
+ JsonObject body = new JsonObject();
+ body.add("status", statusObj);
+ body.addProperty("kind", "Gateway");
+ body.addProperty("apiVersion", GatewayApiConstants.GATEWAY_API_GROUP + "/" + GatewayApiConstants.GATEWAY_API_VERSION);
+
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("name", name);
+ metadata.addProperty("namespace", namespace);
+ body.add("metadata", metadata);
+
+ String patchBody = new Gson().toJson(body);
+ String path = "/apis/" + GatewayApiConstants.GATEWAY_API_GROUP + "/" + GatewayApiConstants.GATEWAY_API_VERSION
+ + "/namespaces/" + namespace + "/gateways/" + name + "/status";
+
+ okhttp3.Request request = new okhttp3.Request.Builder()
+ .url(apiClient.getBasePath() + path)
+ .patch(okhttp3.RequestBody.create(patchBody, okhttp3.MediaType.parse("application/merge-patch+json")))
+ .build();
+
+ try (okhttp3.Response response = apiClient.getHttpClient().newCall(request).execute()) {
+ if (response.isSuccessful()) {
+ LOG.info("Updated Gateway {}/{} status to Accepted=True", namespace, name);
+ } else {
+ String responseBody = Objects.nonNull(response.body()) ? response.body().string() : "empty";
+ LOG.warn("Failed to update Gateway {}/{} status: {} - {}", namespace, name, response.code(), responseBody);
+ }
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to update Gateway status, will retry on next resync", e);
+ }
+ }
+
+ /**
+ * Build the Gateway status conditions array for the patch body.
+ * Includes the accepted condition and preserves non-Accepted conditions
+ * already present in status. Ensures Programmed exists per Gateway API
+ * spec default (Unknown/Pending) if missing.
+ */
+ private JsonArray buildGatewayStatusConditions(final DynamicKubernetesObject gateway,
+ final JsonObject acceptedCondition) {
+ JsonArray conditions = new JsonArray();
+ conditions.add(acceptedCondition);
+
+ boolean hasProgrammed = false;
+ JsonObject raw = gateway.getRaw();
+ if (raw.has("status") && !raw.get("status").isJsonNull()) {
+ JsonObject status = raw.getAsJsonObject("status");
+ if (status.has("conditions") && !status.get("conditions").isJsonNull()) {
+ JsonArray existingConditions = status.getAsJsonArray("conditions");
+ for (JsonElement el : existingConditions) {
+ JsonObject existing = el.getAsJsonObject();
+ String existingType = existing.has("type") ? existing.get("type").getAsString() : null;
+ if ("Programmed".equals(existingType)) {
+ hasProgrammed = true;
+ conditions.add(existing);
+ } else if (!"Accepted".equals(existingType)) {
+ conditions.add(existing);
+ }
+ }
+ }
+ }
+ if (!hasProgrammed) {
+ JsonObject programmedDefault = new JsonObject();
+ programmedDefault.addProperty("type", "Programmed");
+ programmedDefault.addProperty("status", "Unknown");
+ programmedDefault.addProperty("reason", "Pending");
+ programmedDefault.addProperty("message", "Waiting for controller");
+ programmedDefault.addProperty("lastTransitionTime", "1970-01-01T00:00:00Z");
+ conditions.add(programmedDefault);
+ }
+ return conditions;
+ }
+
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java
new file mode 100644
index 000000000000..e77e892a79c0
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java
@@ -0,0 +1,388 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.k8s.reconciler;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.extended.controller.reconciler.Reconciler;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.controller.reconciler.Result;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.k8s.cache.GatewayRouteCache;
+import org.apache.shenyu.k8s.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.common.IngressConfiguration;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.apache.shenyu.k8s.parser.HttpRouteParser;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class HTTPRouteReconciler implements Reconciler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(HTTPRouteReconciler.class);
+
+ private final Lister httpRouteLister;
+
+ private final Lister gatewayLister;
+
+ private final HttpRouteParser httpRouteParser;
+
+ private final ShenyuCacheRepository shenyuCacheRepository;
+
+ private final ApiClient apiClient;
+
+ public HTTPRouteReconciler(final SharedIndexInformer httpRouteInformer,
+ final SharedIndexInformer gatewayInformer,
+ final HttpRouteParser httpRouteParser,
+ final ShenyuCacheRepository shenyuCacheRepository,
+ final ApiClient apiClient) {
+ this.httpRouteLister = new Lister<>(httpRouteInformer.getIndexer());
+ this.gatewayLister = new Lister<>(gatewayInformer.getIndexer());
+ this.httpRouteParser = httpRouteParser;
+ this.shenyuCacheRepository = shenyuCacheRepository;
+ this.apiClient = apiClient;
+ }
+
+ @Override
+ public Result reconcile(final Request request) {
+ LOG.info("Starting to reconcile HTTPRoute {}", request);
+ try {
+ String namespace = request.getNamespace();
+ String routeName = request.getName();
+ DynamicKubernetesObject httpRoute = httpRouteLister.namespace(namespace).get(routeName);
+
+ if (Objects.isNull(httpRoute)) {
+ deleteConfig(namespace, routeName);
+ return new Result(false);
+ }
+
+ if (!isBoundToShenyuGateway(httpRoute)) {
+ LOG.info("HTTPRoute {} is not bound to a ShenYu Gateway, skipping", request);
+ return new Result(false);
+ }
+
+ deleteConfig(namespace, routeName);
+
+ ShenyuMemoryConfig config = httpRouteParser.parse(httpRoute);
+ applyConfig(config);
+
+ bindToGateway(httpRoute);
+
+ updateHTTPRouteStatus(httpRoute);
+
+ LOG.info("HTTPRoute {} reconciled successfully", request);
+ return new Result(false);
+ } catch (Exception e) {
+ LOG.error("Error reconciling HTTPRoute {}, will retry", request, e);
+ return new Result(true);
+ }
+ }
+
+ private boolean isBoundToShenyuGateway(final DynamicKubernetesObject httpRoute) {
+ JsonObject spec = httpRoute.getRaw().getAsJsonObject("spec");
+ if (Objects.isNull(spec) || !spec.has("parentRefs")) {
+ return false;
+ }
+ JsonArray parentRefs = spec.getAsJsonArray("parentRefs");
+ if (Objects.isNull(parentRefs)) {
+ return false;
+ }
+ String routeNamespace = Objects.requireNonNull(httpRoute.getMetadata()).getNamespace();
+ for (JsonElement element : parentRefs) {
+ JsonObject parentRef = element.getAsJsonObject();
+ String parentName = parentRef.has("name") ? parentRef.get("name").getAsString() : null;
+ String parentNamespace = parentRef.has("namespace") ? parentRef.get("namespace").getAsString() : routeNamespace;
+ String sectionName = parentRef.has("sectionName") ? parentRef.get("sectionName").getAsString() : null;
+ if (Objects.isNull(parentName)) {
+ continue;
+ }
+ DynamicKubernetesObject gateway = gatewayLister.namespace(parentNamespace).get(parentName);
+ if (Objects.nonNull(gateway) && GatewayReconciler.isShenyuGateway(gateway)) {
+ // If sectionName is specified, verify the Gateway has a matching listener
+ if (Objects.nonNull(sectionName) && !hasMatchingListener(gateway, sectionName)) {
+ LOG.info("HTTPRoute references sectionName '{}' but Gateway {}/{} has no matching listener", sectionName, parentNamespace, parentName);
+ continue;
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasMatchingListener(final DynamicKubernetesObject gateway, final String sectionName) {
+ JsonObject spec = gateway.getRaw().getAsJsonObject("spec");
+ if (Objects.isNull(spec) || !spec.has("listeners")) {
+ return false;
+ }
+ JsonArray listeners = spec.getAsJsonArray("listeners");
+ if (Objects.isNull(listeners)) {
+ return false;
+ }
+ for (JsonElement listenerElement : listeners) {
+ JsonObject listener = listenerElement.getAsJsonObject();
+ if (listener.has("name") && sectionName.equals(listener.get("name").getAsString())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void deleteConfig(final String namespace, final String routeName) {
+ GatewayRouteCache cache = GatewayRouteCache.getInstance();
+ List selectorIds = cache.removeRouteSelectors(namespace, routeName, PluginEnum.DIVIDE.getName());
+ if (CollectionUtils.isNotEmpty(selectorIds)) {
+ for (String selectorId : selectorIds) {
+ List rules = shenyuCacheRepository.findRuleDataList(selectorId);
+ if (CollectionUtils.isNotEmpty(rules)) {
+ for (RuleData rule : new ArrayList<>(rules)) {
+ shenyuCacheRepository.deleteRuleData(PluginEnum.DIVIDE.getName(), selectorId, rule.getId());
+ }
+ }
+ shenyuCacheRepository.deleteSelectorData(PluginEnum.DIVIDE.getName(), selectorId);
+ }
+ }
+ cache.removeRouteGatewayBinding(namespace, routeName);
+ }
+
+ private void applyConfig(final ShenyuMemoryConfig config) {
+ List routeConfigs = config.getRouteConfigList();
+ if (CollectionUtils.isEmpty(routeConfigs)) {
+ return;
+ }
+ for (IngressConfiguration routeConfig : routeConfigs) {
+ SelectorData selectorData = routeConfig.getSelectorData();
+ shenyuCacheRepository.saveOrUpdateSelectorData(selectorData);
+ for (RuleData ruleData : routeConfig.getRuleDataList()) {
+ shenyuCacheRepository.saveOrUpdateRuleData(ruleData);
+ }
+ }
+ }
+
+ private void bindToGateway(final DynamicKubernetesObject httpRoute) {
+ JsonObject spec = httpRoute.getRaw().getAsJsonObject("spec");
+ if (Objects.isNull(spec) || !spec.has("parentRefs")) {
+ return;
+ }
+ JsonArray parentRefs = spec.getAsJsonArray("parentRefs");
+ String routeNamespace = Objects.requireNonNull(httpRoute.getMetadata()).getNamespace();
+ String routeName = httpRoute.getMetadata().getName();
+
+ for (JsonElement element : parentRefs) {
+ JsonObject parentRef = element.getAsJsonObject();
+ String parentName = parentRef.has("name") ? parentRef.get("name").getAsString() : null;
+ String parentNamespace = parentRef.has("namespace") ? parentRef.get("namespace").getAsString() : routeNamespace;
+ if (Objects.nonNull(parentName)) {
+ GatewayRouteCache.getInstance().bindRouteToGateway(parentNamespace, parentName, routeNamespace, routeName);
+ }
+ }
+ }
+
+ /**
+ * Update HTTPRoute status with Accepted=True and ResolvedRefs=True for each ShenYu-managed parent.
+ * Uses merge-patch on the /status subresource, same approach as GatewayReconciler.
+ * Skips the patch if the status is already up-to-date to avoid triggering an infinite reconcile loop.
+ */
+ private void updateHTTPRouteStatus(final DynamicKubernetesObject httpRoute) {
+ if (isRouteStatusAlreadySet(httpRoute)) {
+ return;
+ }
+ try {
+ final String routeNamespace = Objects.requireNonNull(httpRoute.getMetadata()).getNamespace();
+ final String routeName = httpRoute.getMetadata().getName();
+
+ JsonObject spec = httpRoute.getRaw().getAsJsonObject("spec");
+ if (Objects.isNull(spec) || !spec.has("parentRefs")) {
+ return;
+ }
+ JsonArray parentRefs = spec.getAsJsonArray("parentRefs");
+ JsonArray parentsStatus = buildParentsStatus(parentRefs, routeNamespace);
+
+ if (parentsStatus.size() == 0) {
+ return;
+ }
+
+ // Merge-patch replaces arrays, so preserve status.parents entries owned by other controllers.
+ JsonArray mergedParentsStatus = new JsonArray();
+ JsonObject raw = httpRoute.getRaw();
+ if (raw.has("status") && !raw.get("status").isJsonNull()) {
+ JsonObject status = raw.getAsJsonObject("status");
+ if (status.has("parents") && !status.get("parents").isJsonNull()) {
+ for (JsonElement parentEl : status.getAsJsonArray("parents")) {
+ JsonObject parent = parentEl.getAsJsonObject();
+ if (!parent.has("controllerName")
+ || !GatewayApiConstants.SHENYU_CONTROLLER_NAME.equals(parent.get("controllerName").getAsString())) {
+ mergedParentsStatus.add(parentEl);
+ }
+ }
+ }
+ }
+ parentsStatus.forEach(mergedParentsStatus::add);
+ sendStatusPatch(routeNamespace, routeName, mergedParentsStatus);
+ } catch (Exception e) {
+ LOG.warn("Failed to update HTTPRoute status, will retry on next resync", e);
+ }
+ }
+
+ /**
+ * Check if the HTTPRoute already has both Accepted=True and ResolvedRefs=True conditions
+ * from the ShenYu controller in its status.parents, to avoid unnecessary status patches
+ * that trigger infinite reconcile loops.
+ * Returns true only when ALL ShenYu-managed parents have both conditions set;
+ * returns false if any ShenYu parent is missing either condition or has no conditions at all.
+ */
+ private boolean isRouteStatusAlreadySet(final DynamicKubernetesObject httpRoute) {
+ JsonObject raw = httpRoute.getRaw();
+ if (!raw.has("status") || raw.get("status").isJsonNull()) {
+ return false;
+ }
+ JsonObject status = raw.getAsJsonObject("status");
+ if (!status.has("parents") || status.get("parents").isJsonNull()) {
+ return false;
+ }
+ JsonArray parents = status.getAsJsonArray("parents");
+ for (JsonElement parentElement : parents) {
+ JsonObject parent = parentElement.getAsJsonObject();
+ if (!parent.has("controllerName") || !GatewayApiConstants.SHENYU_CONTROLLER_NAME.equals(parent.get("controllerName").getAsString())) {
+ continue;
+ }
+ if (!parent.has("conditions") || parent.get("conditions").isJsonNull()) {
+ return false;
+ }
+ JsonArray conditions = parent.getAsJsonArray("conditions");
+ boolean hasAccepted = false;
+ boolean hasResolvedRefs = false;
+ for (JsonElement condElement : conditions) {
+ JsonObject cond = condElement.getAsJsonObject();
+ String type = cond.has("type") ? cond.get("type").getAsString() : null;
+ String condStatus = cond.has("status") ? cond.get("status").getAsString() : null;
+ if ("True".equals(condStatus)) {
+ if ("Accepted".equals(type)) {
+ hasAccepted = true;
+ } else if ("ResolvedRefs".equals(type)) {
+ hasResolvedRefs = true;
+ }
+ }
+ }
+ if (!hasAccepted || !hasResolvedRefs) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private JsonArray buildParentsStatus(final JsonArray parentRefs, final String routeNamespace) {
+ final JsonArray parentsStatus = new JsonArray();
+ for (JsonElement element : parentRefs) {
+ JsonObject parentRef = element.getAsJsonObject();
+ String parentName = parentRef.has("name") ? parentRef.get("name").getAsString() : null;
+ String parentNamespace = parentRef.has("namespace") ? parentRef.get("namespace").getAsString() : routeNamespace;
+
+ if (Objects.isNull(parentName)) {
+ continue;
+ }
+ DynamicKubernetesObject gateway = gatewayLister.namespace(parentNamespace).get(parentName);
+ if (Objects.isNull(gateway) || !GatewayReconciler.isShenyuGateway(gateway)) {
+ continue;
+ }
+ parentsStatus.add(buildParentStatus(parentNamespace, parentName));
+ }
+ return parentsStatus;
+ }
+
+ private JsonObject buildParentStatus(final String parentNamespace, final String parentName) {
+ JsonObject parentRefStatus = new JsonObject();
+ parentRefStatus.addProperty("group", GatewayApiConstants.GATEWAY_API_GROUP);
+ parentRefStatus.addProperty("kind", GatewayApiConstants.GATEWAY_KIND);
+ parentRefStatus.addProperty("namespace", parentNamespace);
+ parentRefStatus.addProperty("name", parentName);
+
+ String now = Instant.now().toString();
+ JsonArray conditions = buildStatusConditions(now);
+
+ JsonObject parentStatus = new JsonObject();
+ parentStatus.add("parentRef", parentRefStatus);
+ parentStatus.addProperty("controllerName", GatewayApiConstants.SHENYU_CONTROLLER_NAME);
+ parentStatus.add("conditions", conditions);
+ return parentStatus;
+ }
+
+ private JsonArray buildStatusConditions(final String now) {
+ final JsonArray conditions = new JsonArray();
+ conditions.add(buildCondition("Accepted", "Route was accepted by the ShenYu controller", now));
+ conditions.add(buildCondition("ResolvedRefs", "All references resolved", now));
+ return conditions;
+ }
+
+ private JsonObject buildCondition(final String type, final String message, final String now) {
+ JsonObject condition = new JsonObject();
+ condition.addProperty("type", type);
+ condition.addProperty("status", "True");
+ condition.addProperty("reason", type);
+ condition.addProperty("message", message);
+ condition.addProperty("lastTransitionTime", now);
+ return condition;
+ }
+
+ private void sendStatusPatch(final String routeNamespace, final String routeName,
+ final JsonArray parentsStatus) throws Exception {
+ JsonObject statusObj = new JsonObject();
+ statusObj.add("parents", parentsStatus);
+
+ JsonObject body = new JsonObject();
+ body.add("status", statusObj);
+ body.addProperty("kind", GatewayApiConstants.HTTP_ROUTE_KIND);
+ body.addProperty("apiVersion", GatewayApiConstants.GATEWAY_API_GROUP + "/" + GatewayApiConstants.GATEWAY_API_VERSION);
+
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("name", routeName);
+ metadata.addProperty("namespace", routeNamespace);
+ body.add("metadata", metadata);
+
+ String patchBody = new Gson().toJson(body);
+ String path = "/apis/" + GatewayApiConstants.GATEWAY_API_GROUP + "/" + GatewayApiConstants.GATEWAY_API_VERSION
+ + "/namespaces/" + routeNamespace + "/httproutes/" + routeName + "/status";
+
+ okhttp3.Request httpRequest = new okhttp3.Request.Builder()
+ .url(apiClient.getBasePath() + path)
+ .patch(okhttp3.RequestBody.create(patchBody, okhttp3.MediaType.parse("application/merge-patch+json")))
+ .build();
+
+ try (okhttp3.Response response = apiClient.getHttpClient().newCall(httpRequest).execute()) {
+ if (response.isSuccessful()) {
+ LOG.info("Updated HTTPRoute {}/{} status to Accepted=True", routeNamespace, routeName);
+ } else {
+ String responseBody = Objects.nonNull(response.body()) ? response.body().string() : "empty";
+ LOG.warn("Failed to update HTTPRoute {}/{} status: {} - {}", routeNamespace, routeName, response.code(), responseBody);
+ }
+ }
+ }
+}
diff --git a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/GatewayReconcilerTest.java b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/GatewayReconcilerTest.java
new file mode 100644
index 000000000000..f7fe59b13fce
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/GatewayReconcilerTest.java
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.k8s;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.controller.reconciler.Result;
+import io.kubernetes.client.extended.workqueue.RateLimitingQueue;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.cache.Indexer;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Protocol;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.k8s.cache.GatewayRouteCache;
+import org.apache.shenyu.k8s.reconciler.GatewayReconciler;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Gateway Reconciler Test.
+ */
+public final class GatewayReconcilerTest {
+
+ @BeforeEach
+ public void setUp() {
+ GatewayRouteCache.getInstance().clear();
+ }
+
+ /**
+ * Test ShenYu Gateway creation.
+ */
+ @Test
+ public void testReconcileShenYuGatewayCreation() throws Exception {
+ // mock gateway indexer
+ SharedIndexInformer gatewayInformer = mock(SharedIndexInformer.class);
+ Indexer gatewayIndexer = mock(Indexer.class);
+ DynamicKubernetesObject gateway = buildGateway("mockedNamespace", "shenyu-gateway", "shenyu");
+ when(gatewayIndexer.getByKey("mockedNamespace/shenyu-gateway")).thenReturn(gateway);
+ when(gatewayInformer.getIndexer()).thenReturn(gatewayIndexer);
+
+ // mock httpRoute indexer with a route referencing this gateway
+ SharedIndexInformer httpRouteInformer = mock(SharedIndexInformer.class);
+ Indexer httpRouteIndexer = mock(Indexer.class);
+ DynamicKubernetesObject httpRoute = buildHTTPRoute("mockedNamespace", "test-route",
+ "mockedNamespace", "shenyu-gateway", "testService", 8189);
+ when(httpRouteIndexer.getByKey("mockedNamespace/test-route")).thenReturn(httpRoute);
+ when(httpRouteIndexer.byIndex("namespace", "mockedNamespace")).thenReturn(List.of(httpRoute));
+ when(httpRouteInformer.getIndexer()).thenReturn(httpRouteIndexer);
+
+ ShenyuCacheRepository shenyuCacheRepository = mock(ShenyuCacheRepository.class);
+ RateLimitingQueue httpRouteWorkQueue = mock(RateLimitingQueue.class);
+ ApiClient apiClient = mock(ApiClient.class);
+ OkHttpClient httpClient = mock(OkHttpClient.class);
+ when(apiClient.getHttpClient()).thenReturn(httpClient);
+ when(apiClient.getBasePath()).thenReturn("http://localhost:8080");
+ okhttp3.Call call = mock(okhttp3.Call.class);
+ when(httpClient.newCall(any(okhttp3.Request.class))).thenReturn(call);
+ Response successResponse = new Response.Builder()
+ .request(new okhttp3.Request.Builder().url("http://localhost").build())
+ .protocol(Protocol.HTTP_1_1).code(200).message("OK")
+ .body(ResponseBody.create("{}", MediaType.parse("application/json"))).build();
+ when(call.execute()).thenReturn(successResponse);
+
+ GatewayReconciler gatewayReconciler = new GatewayReconciler(gatewayInformer, httpRouteInformer,
+ shenyuCacheRepository, httpRouteWorkQueue, apiClient);
+
+ Result result = gatewayReconciler.reconcile(new Request("mockedNamespace", "shenyu-gateway"));
+ Assertions.assertEquals(new Result(false), result);
+ verify(httpRouteWorkQueue).add(new Request("mockedNamespace", "test-route"));
+ verify(httpClient).newCall(any(okhttp3.Request.class));
+ }
+
+ /**
+ * Test non-ShenYu Gateway creation: should skip without re-queuing HTTPRoutes.
+ */
+ @Test
+ public void testReconcileNonShenYuGatewayCreation() {
+ SharedIndexInformer gatewayInformer = mock(SharedIndexInformer.class);
+ Indexer gatewayIndexer = mock(Indexer.class);
+ DynamicKubernetesObject gateway = buildGateway("mockedNamespace", "other-gateway", "other-class");
+ when(gatewayIndexer.getByKey("mockedNamespace/other-gateway")).thenReturn(gateway);
+ when(gatewayInformer.getIndexer()).thenReturn(gatewayIndexer);
+
+ SharedIndexInformer httpRouteInformer = mock(SharedIndexInformer.class);
+ Indexer httpRouteIndexer = mock(Indexer.class);
+ when(httpRouteInformer.getIndexer()).thenReturn(httpRouteIndexer);
+
+ ShenyuCacheRepository shenyuCacheRepository = mock(ShenyuCacheRepository.class);
+ RateLimitingQueue httpRouteWorkQueue = mock(RateLimitingQueue.class);
+ ApiClient apiClient = mock(ApiClient.class);
+
+ GatewayReconciler gatewayReconciler = new GatewayReconciler(gatewayInformer, httpRouteInformer,
+ shenyuCacheRepository, httpRouteWorkQueue, apiClient);
+
+ Result result = gatewayReconciler.reconcile(new Request("mockedNamespace", "other-gateway"));
+ Assertions.assertEquals(new Result(false), result);
+ verify(httpRouteWorkQueue, never()).add(any());
+ }
+
+ /**
+ * Test Gateway deletion: should cascade delete ShenYu config for associated routes.
+ */
+ @Test
+ public void testReconcileGatewayDeletion() {
+ // gateway not found in indexer → treated as deletion
+ SharedIndexInformer gatewayInformer = mock(SharedIndexInformer.class);
+ Indexer gatewayIndexer = mock(Indexer.class);
+ when(gatewayIndexer.getByKey("mockedNamespace/shenyu-gateway")).thenReturn(null);
+ when(gatewayInformer.getIndexer()).thenReturn(gatewayIndexer);
+
+ SharedIndexInformer httpRouteInformer = mock(SharedIndexInformer.class);
+ Indexer httpRouteIndexer = mock(Indexer.class);
+ when(httpRouteInformer.getIndexer()).thenReturn(httpRouteIndexer);
+
+ // pre-populate GatewayRouteCache with a bound route
+ GatewayRouteCache cache = GatewayRouteCache.getInstance();
+ cache.bindRouteToGateway("mockedNamespace", "shenyu-gateway", "mockedNamespace", "test-route");
+ String selectorId = cache.generateSelectorId();
+ cache.addRouteSelector("mockedNamespace", "test-route", "divide", selectorId);
+
+ ShenyuCacheRepository shenyuCacheRepository = mock(ShenyuCacheRepository.class);
+ RuleData ruleData = mock(RuleData.class);
+ when(ruleData.getId()).thenReturn("rule-1");
+ when(shenyuCacheRepository.findRuleDataList(selectorId)).thenReturn(List.of(ruleData));
+
+ RateLimitingQueue httpRouteWorkQueue = mock(RateLimitingQueue.class);
+ ApiClient apiClient = mock(ApiClient.class);
+ GatewayReconciler gatewayReconciler = new GatewayReconciler(gatewayInformer, httpRouteInformer,
+ shenyuCacheRepository, httpRouteWorkQueue, apiClient);
+
+ Result result = gatewayReconciler.reconcile(new Request("mockedNamespace", "shenyu-gateway"));
+ Assertions.assertEquals(new Result(false), result);
+ verify(shenyuCacheRepository).deleteRuleData("divide", selectorId, "rule-1");
+ verify(shenyuCacheRepository).deleteSelectorData("divide", selectorId);
+ }
+
+ /**
+ * Test Gateway deletion with no associated routes: should not throw or delete anything.
+ */
+ @Test
+ public void testReconcileGatewayDeletionWithNoAssociatedRoutes() {
+ SharedIndexInformer gatewayInformer = mock(SharedIndexInformer.class);
+ Indexer gatewayIndexer = mock(Indexer.class);
+ when(gatewayIndexer.getByKey("mockedNamespace/empty-gateway")).thenReturn(null);
+ when(gatewayInformer.getIndexer()).thenReturn(gatewayIndexer);
+
+ SharedIndexInformer httpRouteInformer = mock(SharedIndexInformer.class);
+ Indexer httpRouteIndexer = mock(Indexer.class);
+ when(httpRouteInformer.getIndexer()).thenReturn(httpRouteIndexer);
+
+ ShenyuCacheRepository shenyuCacheRepository = mock(ShenyuCacheRepository.class);
+ RateLimitingQueue httpRouteWorkQueue = mock(RateLimitingQueue.class);
+ ApiClient apiClient = mock(ApiClient.class);
+
+ GatewayReconciler gatewayReconciler = new GatewayReconciler(gatewayInformer, httpRouteInformer,
+ shenyuCacheRepository, httpRouteWorkQueue, apiClient);
+
+ Result result = gatewayReconciler.reconcile(new Request("mockedNamespace", "empty-gateway"));
+ Assertions.assertEquals(new Result(false), result);
+ verify(shenyuCacheRepository, never()).deleteSelectorData(any(), any());
+ verify(shenyuCacheRepository, never()).deleteRuleData(any(), any(), any());
+ }
+
+ /**
+ * Test that status update is skipped when Gateway already has Accepted=True condition.
+ */
+ @Test
+ public void testReconcileGatewayAlreadyAccepted() {
+ // Build Accepted=True status JSON first
+ JsonObject acceptedCondition = new JsonObject();
+ acceptedCondition.addProperty("type", "Accepted");
+ acceptedCondition.addProperty("status", "True");
+ acceptedCondition.addProperty("reason", "Accepted");
+ acceptedCondition.addProperty("message", "Already accepted");
+ JsonArray conditions = new JsonArray();
+ conditions.add(acceptedCondition);
+ JsonObject statusObj = new JsonObject();
+ statusObj.add("conditions", conditions);
+
+ DynamicKubernetesObject gateway = buildGateway("mockedNamespace", "shenyu-gateway", "shenyu");
+ gateway.getRaw().add("status", statusObj);
+
+ Indexer gatewayIndexer = mock(Indexer.class);
+ when(gatewayIndexer.getByKey("mockedNamespace/shenyu-gateway")).thenReturn(gateway);
+ SharedIndexInformer gatewayInformer = mock(SharedIndexInformer.class);
+ when(gatewayInformer.getIndexer()).thenReturn(gatewayIndexer);
+
+ SharedIndexInformer httpRouteInformer = mock(SharedIndexInformer.class);
+ Indexer httpRouteIndexer = mock(Indexer.class);
+ when(httpRouteInformer.getIndexer()).thenReturn(httpRouteIndexer);
+
+ ShenyuCacheRepository shenyuCacheRepository = mock(ShenyuCacheRepository.class);
+ RateLimitingQueue httpRouteWorkQueue = mock(RateLimitingQueue.class);
+ ApiClient apiClient = mock(ApiClient.class);
+ OkHttpClient httpClient = mock(OkHttpClient.class);
+ when(apiClient.getHttpClient()).thenReturn(httpClient);
+
+ GatewayReconciler gatewayReconciler = new GatewayReconciler(gatewayInformer, httpRouteInformer,
+ shenyuCacheRepository, httpRouteWorkQueue, apiClient);
+
+ Result result = gatewayReconciler.reconcile(new Request("mockedNamespace", "shenyu-gateway"));
+ Assertions.assertEquals(new Result(false), result);
+ verify(httpClient, never()).newCall(any(okhttp3.Request.class));
+ }
+
+ private DynamicKubernetesObject buildGateway(final String namespace, final String name,
+ final String gatewayClassName) {
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("namespace", namespace);
+ metadata.addProperty("name", name);
+
+ JsonObject spec = new JsonObject();
+ spec.addProperty("gatewayClassName", gatewayClassName);
+
+ JsonObject raw = new JsonObject();
+ raw.addProperty("apiVersion", "gateway.networking.k8s.io/v1");
+ raw.addProperty("kind", "Gateway");
+ raw.add("metadata", metadata);
+ raw.add("spec", spec);
+ return new DynamicKubernetesObject(raw);
+ }
+
+ private DynamicKubernetesObject buildHTTPRoute(final String routeNamespace, final String routeName,
+ final String gatewayNamespace, final String gatewayName,
+ final String serviceName, final int port) {
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("namespace", routeNamespace);
+ metadata.addProperty("name", routeName);
+
+ JsonObject parentRef = new JsonObject();
+ parentRef.addProperty("name", gatewayName);
+ parentRef.addProperty("namespace", gatewayNamespace);
+ JsonArray parentRefs = new JsonArray();
+ parentRefs.add(parentRef);
+
+ JsonObject backendRef = new JsonObject();
+ backendRef.addProperty("name", serviceName);
+ backendRef.addProperty("port", port);
+ JsonArray backendRefs = new JsonArray();
+ backendRefs.add(backendRef);
+
+ JsonObject rule = new JsonObject();
+ rule.add("backendRefs", backendRefs);
+ JsonArray rules = new JsonArray();
+ rules.add(rule);
+
+ JsonObject spec = new JsonObject();
+ spec.add("parentRefs", parentRefs);
+ spec.add("rules", rules);
+
+ JsonObject raw = new JsonObject();
+ raw.addProperty("apiVersion", "gateway.networking.k8s.io/v1");
+ raw.addProperty("kind", "HTTPRoute");
+ raw.add("metadata", metadata);
+ raw.add("spec", spec);
+ return new DynamicKubernetesObject(raw);
+ }
+}
diff --git a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/HTTPRouteReconcilerTest.java b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/HTTPRouteReconcilerTest.java
new file mode 100644
index 000000000000..c58bc31c86e3
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/HTTPRouteReconcilerTest.java
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.k8s;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.controller.reconciler.Result;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.cache.Indexer;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.models.V1EndpointAddress;
+import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1EndpointsBuilder;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Protocol;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import org.apache.shenyu.k8s.cache.GatewayRouteCache;
+import org.apache.shenyu.k8s.parser.HttpRouteParser;
+import org.apache.shenyu.k8s.reconciler.HTTPRouteReconciler;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * HTTPRoute Reconciler Test.
+ */
+public final class HTTPRouteReconcilerTest {
+
+ @BeforeEach
+ public void setUp() {
+ GatewayRouteCache.getInstance().clear();
+ }
+
+ /**
+ * Test HTTPRoute bound to a ShenYu Gateway: should create selector and rule.
+ */
+ @Test
+ public void testReconcileBoundHTTPRoute() throws Exception {
+ // mock endpoints indexer and lister
+ Indexer endpointsIndexer = mock(Indexer.class);
+ V1Endpoints mockedEndpoints = new V1EndpointsBuilder().withKind("Endpoints")
+ .withNewMetadata().withNamespace("mockedNamespace").withName("testService").endMetadata()
+ .withSubsets(new V1EndpointSubsetBuilder().withAddresses(new V1EndpointAddress().ip("127.0.0.1")).build())
+ .build();
+ when(endpointsIndexer.getByKey("mockedNamespace/testService")).thenReturn(mockedEndpoints);
+ Lister endpointsLister = new Lister<>(endpointsIndexer);
+ HttpRouteParser httpRouteParser = new HttpRouteParser(endpointsLister);
+
+ // mock gateway indexer
+ SharedIndexInformer gatewayInformer = mock(SharedIndexInformer.class);
+ Indexer gatewayIndexer = mock(Indexer.class);
+ DynamicKubernetesObject gateway = buildGateway("mockedNamespace", "shenyu-gateway", "shenyu");
+ when(gatewayIndexer.getByKey("mockedNamespace/shenyu-gateway")).thenReturn(gateway);
+ when(gatewayInformer.getIndexer()).thenReturn(gatewayIndexer);
+
+ // mock httpRoute indexer
+ SharedIndexInformer httpRouteInformer = mock(SharedIndexInformer.class);
+ Indexer httpRouteIndexer = mock(Indexer.class);
+ DynamicKubernetesObject httpRoute = buildHTTPRoute("mockedNamespace", "test-route",
+ "mockedNamespace", "shenyu-gateway", "testService", 8189, "/**");
+ when(httpRouteIndexer.getByKey("mockedNamespace/test-route")).thenReturn(httpRoute);
+ when(httpRouteInformer.getIndexer()).thenReturn(httpRouteIndexer);
+
+ // mock ApiClient for status update
+ ApiClient apiClient = mock(ApiClient.class);
+ when(apiClient.getBasePath()).thenReturn("http://localhost:8080");
+ OkHttpClient httpClient = mock(OkHttpClient.class);
+ when(apiClient.getHttpClient()).thenReturn(httpClient);
+ okhttp3.Call call = mock(okhttp3.Call.class);
+ when(httpClient.newCall(any(okhttp3.Request.class))).thenReturn(call);
+ Response response = new Response.Builder()
+ .request(new okhttp3.Request.Builder().url("http://localhost").build())
+ .protocol(Protocol.HTTP_1_1).code(200).message("OK")
+ .body(ResponseBody.create("", MediaType.parse("application/json"))).build();
+ when(call.execute()).thenReturn(response);
+
+ ShenyuCacheRepository shenyuCacheRepository = mock(ShenyuCacheRepository.class);
+ HTTPRouteReconciler httpRouteReconciler = new HTTPRouteReconciler(httpRouteInformer, gatewayInformer,
+ httpRouteParser, shenyuCacheRepository, apiClient);
+
+ Result result = httpRouteReconciler.reconcile(new Request("mockedNamespace", "test-route"));
+ Assertions.assertEquals(new Result(false), result);
+ verify(shenyuCacheRepository).saveOrUpdateSelectorData(any());
+ verify(shenyuCacheRepository).saveOrUpdateRuleData(any());
+ verify(httpClient).newCall(any(okhttp3.Request.class));
+ }
+
+ /**
+ * Test HTTPRoute not bound to any ShenYu Gateway: should skip without creating selector/rule.
+ */
+ @Test
+ public void testReconcileUnboundHTTPRoute() {
+ Indexer endpointsIndexer = mock(Indexer.class);
+ Lister endpointsLister = new Lister<>(endpointsIndexer);
+ HttpRouteParser httpRouteParser = new HttpRouteParser(endpointsLister);
+
+ // mock gateway indexer with non-shenyu gateway
+ SharedIndexInformer gatewayInformer = mock(SharedIndexInformer.class);
+ Indexer gatewayIndexer = mock(Indexer.class);
+ DynamicKubernetesObject otherGateway = buildGateway("mockedNamespace", "other-gateway", "other-class");
+ when(gatewayIndexer.getByKey("mockedNamespace/other-gateway")).thenReturn(otherGateway);
+ when(gatewayInformer.getIndexer()).thenReturn(gatewayIndexer);
+
+ // mock httpRoute indexer
+ SharedIndexInformer httpRouteInformer = mock(SharedIndexInformer.class);
+ Indexer httpRouteIndexer = mock(Indexer.class);
+ DynamicKubernetesObject httpRoute = buildHTTPRoute("mockedNamespace", "test-route",
+ "mockedNamespace", "other-gateway", "testService", 8189, "/**");
+ when(httpRouteIndexer.getByKey("mockedNamespace/test-route")).thenReturn(httpRoute);
+ when(httpRouteInformer.getIndexer()).thenReturn(httpRouteIndexer);
+
+ ShenyuCacheRepository shenyuCacheRepository = mock(ShenyuCacheRepository.class);
+ ApiClient apiClient = mock(ApiClient.class);
+ OkHttpClient httpClient = mock(OkHttpClient.class);
+ when(apiClient.getHttpClient()).thenReturn(httpClient);
+ HTTPRouteReconciler httpRouteReconciler = new HTTPRouteReconciler(httpRouteInformer, gatewayInformer,
+ httpRouteParser, shenyuCacheRepository, apiClient);
+
+ Result result = httpRouteReconciler.reconcile(new Request("mockedNamespace", "test-route"));
+ Assertions.assertEquals(new Result(false), result);
+ verify(shenyuCacheRepository, never()).saveOrUpdateSelectorData(any());
+ verify(shenyuCacheRepository, never()).saveOrUpdateRuleData(any());
+ verify(httpClient, never()).newCall(any(okhttp3.Request.class));
+ }
+
+ /**
+ * Test HTTPRoute deletion: should clean up selector and rule data.
+ */
+ @Test
+ public void testReconcileHTTPRouteDeletion() {
+ Indexer endpointsIndexer = mock(Indexer.class);
+ Lister endpointsLister = new Lister<>(endpointsIndexer);
+ HttpRouteParser httpRouteParser = new HttpRouteParser(endpointsLister);
+
+ // httpRoute not found in indexer → treated as deletion
+ SharedIndexInformer httpRouteInformer = mock(SharedIndexInformer.class);
+ Indexer httpRouteIndexer = mock(Indexer.class);
+ when(httpRouteIndexer.getByKey("mockedNamespace/test-route")).thenReturn(null);
+ when(httpRouteInformer.getIndexer()).thenReturn(httpRouteIndexer);
+
+ SharedIndexInformer gatewayInformer = mock(SharedIndexInformer.class);
+ Indexer gatewayIndexer = mock(Indexer.class);
+ when(gatewayInformer.getIndexer()).thenReturn(gatewayIndexer);
+
+ ShenyuCacheRepository shenyuCacheRepository = mock(ShenyuCacheRepository.class);
+ ApiClient apiClient = mock(ApiClient.class);
+ HTTPRouteReconciler httpRouteReconciler = new HTTPRouteReconciler(httpRouteInformer, gatewayInformer,
+ httpRouteParser, shenyuCacheRepository, apiClient);
+
+ Result result = httpRouteReconciler.reconcile(new Request("mockedNamespace", "test-route"));
+ Assertions.assertEquals(new Result(false), result);
+ // No exception should be thrown; deleteConfig handles empty cache gracefully
+ }
+
+ private DynamicKubernetesObject buildGateway(final String namespace, final String name,
+ final String gatewayClassName) {
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("namespace", namespace);
+ metadata.addProperty("name", name);
+
+ JsonObject spec = new JsonObject();
+ spec.addProperty("gatewayClassName", gatewayClassName);
+
+ JsonObject raw = new JsonObject();
+ raw.addProperty("apiVersion", "gateway.networking.k8s.io/v1");
+ raw.addProperty("kind", "Gateway");
+ raw.add("metadata", metadata);
+ raw.add("spec", spec);
+ return new DynamicKubernetesObject(raw);
+ }
+
+ private DynamicKubernetesObject buildHTTPRoute(final String routeNamespace, final String routeName,
+ final String gatewayNamespace, final String gatewayName,
+ final String serviceName, final int port,
+ final String pathValue) {
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("namespace", routeNamespace);
+ metadata.addProperty("name", routeName);
+
+ JsonObject parentRef = new JsonObject();
+ parentRef.addProperty("name", gatewayName);
+ parentRef.addProperty("namespace", gatewayNamespace);
+ JsonArray parentRefs = new JsonArray();
+ parentRefs.add(parentRef);
+
+ JsonObject backendRef = new JsonObject();
+ backendRef.addProperty("name", serviceName);
+ backendRef.addProperty("port", port);
+ JsonArray backendRefs = new JsonArray();
+ backendRefs.add(backendRef);
+
+ JsonObject pathMatch = new JsonObject();
+ pathMatch.addProperty("type", "PathPrefix");
+ pathMatch.addProperty("value", pathValue);
+ JsonObject match = new JsonObject();
+ match.add("path", pathMatch);
+ JsonArray matches = new JsonArray();
+ matches.add(match);
+
+ JsonObject rule = new JsonObject();
+ rule.add("backendRefs", backendRefs);
+ rule.add("matches", matches);
+ JsonArray rules = new JsonArray();
+ rules.add(rule);
+
+ JsonObject spec = new JsonObject();
+ spec.add("parentRefs", parentRefs);
+ spec.add("rules", rules);
+
+ JsonObject raw = new JsonObject();
+ raw.addProperty("apiVersion", "gateway.networking.k8s.io/v1");
+ raw.addProperty("kind", "HTTPRoute");
+ raw.add("metadata", metadata);
+ raw.add("spec", spec);
+ return new DynamicKubernetesObject(raw);
+ }
+}
diff --git a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/HttpRouteParserTest.java b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/HttpRouteParserTest.java
new file mode 100644
index 000000000000..8b95c7e897e9
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/HttpRouteParserTest.java
@@ -0,0 +1,439 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.k8s;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.informer.cache.Indexer;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.models.V1EndpointAddress;
+import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1EndpointsBuilder;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.apache.shenyu.common.dto.ConditionData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.enums.OperatorEnum;
+import org.apache.shenyu.common.enums.ParamTypeEnum;
+import org.apache.shenyu.k8s.common.IngressConfiguration;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.apache.shenyu.k8s.parser.HttpRouteParser;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Objects;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * HttpRouteParser Test.
+ */
+public final class HttpRouteParserTest {
+
+ private static final String NAMESPACE = "test-ns";
+
+ private static final String SERVICE_NAME = "test-service";
+
+ private static final int SERVICE_PORT = 8189;
+
+ /**
+ * Test parse with path prefix match.
+ */
+ @Test
+ public void testParseWithPathPrefix() {
+ Lister endpointsLister = mockEndpointsLister();
+ HttpRouteParser parser = new HttpRouteParser(endpointsLister);
+
+ DynamicKubernetesObject httpRoute = buildHTTPRoute(NAMESPACE, "test-route",
+ NAMESPACE, "shenyu-gateway", SERVICE_NAME, SERVICE_PORT,
+ "/api/**", "PathPrefix", null, null);
+ ShenyuMemoryConfig config = parser.parse(httpRoute);
+
+ List selectors = extractSelectors(config);
+ List rules = extractRules(config);
+ Assertions.assertEquals(1, selectors.size());
+ Assertions.assertEquals(1, rules.size());
+
+ ConditionData pathCondition = config.getRouteConfigList().get(0).getSelectorData().getConditionList().get(0);
+ Assertions.assertEquals(ParamTypeEnum.URI.getName(), pathCondition.getParamType());
+ Assertions.assertEquals(OperatorEnum.STARTS_WITH.getAlias(), pathCondition.getOperator());
+ Assertions.assertEquals("/api/**", pathCondition.getParamValue());
+ }
+
+ /**
+ * Test parse with exact path match.
+ */
+ @Test
+ public void testParseWithExactPath() {
+ Lister endpointsLister = mockEndpointsLister();
+ HttpRouteParser parser = new HttpRouteParser(endpointsLister);
+
+ DynamicKubernetesObject httpRoute = buildHTTPRoute(NAMESPACE, "test-route",
+ NAMESPACE, "shenyu-gateway", SERVICE_NAME, SERVICE_PORT,
+ "/api/v1/test", "Exact", null, null);
+ ShenyuMemoryConfig config = parser.parse(httpRoute);
+
+ ConditionData pathCondition = config.getRouteConfigList().get(0).getSelectorData().getConditionList().get(0);
+ Assertions.assertEquals(OperatorEnum.EQ.getAlias(), pathCondition.getOperator());
+ }
+
+ /**
+ * Test parse with regex path match.
+ */
+ @Test
+ public void testParseWithRegexPath() {
+ Lister endpointsLister = mockEndpointsLister();
+ HttpRouteParser parser = new HttpRouteParser(endpointsLister);
+
+ DynamicKubernetesObject httpRoute = buildHTTPRoute(NAMESPACE, "test-route",
+ NAMESPACE, "shenyu-gateway", SERVICE_NAME, SERVICE_PORT,
+ "/api/v[0-9]+/.*", "RegularExpression", null, null);
+ ShenyuMemoryConfig config = parser.parse(httpRoute);
+
+ ConditionData pathCondition = config.getRouteConfigList().get(0).getSelectorData().getConditionList().get(0);
+ Assertions.assertEquals(OperatorEnum.MATCH.getAlias(), pathCondition.getOperator());
+ }
+
+ /**
+ * Test parse with hostname conditions.
+ * Each hostname should generate a separate selector+rule (one hostname per selector).
+ */
+ @Test
+ public void testParseWithHostnames() {
+ Lister endpointsLister = mockEndpointsLister();
+ HttpRouteParser parser = new HttpRouteParser(endpointsLister);
+
+ DynamicKubernetesObject httpRoute = buildHTTPRouteWithHostnames(NAMESPACE, "test-route",
+ NAMESPACE, "shenyu-gateway", SERVICE_NAME, SERVICE_PORT,
+ "/**", "PathPrefix", new String[]{"example.com", "api.example.com"});
+ ShenyuMemoryConfig config = parser.parse(httpRoute);
+
+ // Should have 2 selectors (one per hostname), each with 1 host + 1 path condition
+ List routeConfigs = config.getRouteConfigList();
+ Assertions.assertEquals(2, routeConfigs.size());
+
+ for (IngressConfiguration routeConfig : routeConfigs) {
+ List conditions = routeConfig.getSelectorData().getConditionList();
+ long hostConditions = conditions.stream()
+ .filter(c -> ParamTypeEnum.DOMAIN.getName().equals(c.getParamType()))
+ .count();
+ Assertions.assertEquals(1, hostConditions);
+
+ long pathConditions = conditions.stream()
+ .filter(c -> ParamTypeEnum.URI.getName().equals(c.getParamType()))
+ .count();
+ Assertions.assertEquals(1, pathConditions);
+ }
+ }
+
+ /**
+ * Test parse with header match.
+ */
+ @Test
+ public void testParseWithHeaderMatch() {
+ Lister endpointsLister = mockEndpointsLister();
+ HttpRouteParser parser = new HttpRouteParser(endpointsLister);
+
+ DynamicKubernetesObject httpRoute = buildHTTPRoute(NAMESPACE, "test-route",
+ NAMESPACE, "shenyu-gateway", SERVICE_NAME, SERVICE_PORT,
+ "/**", "PathPrefix", "X-Custom-Header", "test-value");
+ ShenyuMemoryConfig config = parser.parse(httpRoute);
+
+ List conditions = config.getRouteConfigList().get(0).getSelectorData().getConditionList();
+ long headerConditions = conditions.stream()
+ .filter(c -> ParamTypeEnum.HEADER.getName().equals(c.getParamType()))
+ .count();
+ Assertions.assertEquals(1, headerConditions);
+
+ ConditionData headerCondition = conditions.stream()
+ .filter(c -> ParamTypeEnum.HEADER.getName().equals(c.getParamType()))
+ .findFirst().orElse(null);
+ Assertions.assertNotNull(headerCondition);
+ Assertions.assertEquals("X-Custom-Header", headerCondition.getParamName());
+ Assertions.assertEquals("test-value", headerCondition.getParamValue());
+ Assertions.assertEquals(OperatorEnum.EQ.getAlias(), headerCondition.getOperator());
+ }
+
+ /**
+ * Test parse with no rules: should return empty config.
+ */
+ @Test
+ public void testParseWithNoRules() {
+ Lister endpointsLister = mockEndpointsLister();
+ HttpRouteParser parser = new HttpRouteParser(endpointsLister);
+
+ DynamicKubernetesObject httpRoute = buildHTTPRouteNoRules(NAMESPACE, "empty-route");
+ ShenyuMemoryConfig config = parser.parse(httpRoute);
+
+ Assertions.assertTrue(Objects.isNull(config.getRouteConfigList()) || config.getRouteConfigList().isEmpty());
+ }
+
+ /**
+ * Test parse with no matching endpoints: should return empty upstream list but still create selector/rule.
+ */
+ @Test
+ public void testParseWithNoEndpoints() {
+ Indexer endpointsIndexer = mock(Indexer.class);
+ when(endpointsIndexer.getByKey(NAMESPACE + "/" + SERVICE_NAME)).thenReturn(null);
+ Lister endpointsLister = new Lister<>(endpointsIndexer);
+
+ HttpRouteParser parser = new HttpRouteParser(endpointsLister);
+
+ DynamicKubernetesObject httpRoute = buildHTTPRoute(NAMESPACE, "test-route",
+ NAMESPACE, "shenyu-gateway", SERVICE_NAME, SERVICE_PORT,
+ "/**", "PathPrefix", null, null);
+ ShenyuMemoryConfig config = parser.parse(httpRoute);
+
+ // Selector should still exist but with empty upstream handle
+ List selectors = extractSelectors(config);
+ Assertions.assertEquals(1, selectors.size());
+ }
+
+ /**
+ * Test parse with query param match.
+ */
+ @Test
+ public void testParseWithQueryParam() {
+ Lister endpointsLister = mockEndpointsLister();
+ HttpRouteParser parser = new HttpRouteParser(endpointsLister);
+
+ DynamicKubernetesObject httpRoute = buildHTTPRouteWithQueryParams(NAMESPACE, "test-route",
+ NAMESPACE, "shenyu-gateway", SERVICE_NAME, SERVICE_PORT,
+ "/**", "PathPrefix", "debug", "true", "Exact");
+ ShenyuMemoryConfig config = parser.parse(httpRoute);
+
+ List conditions = config.getRouteConfigList().get(0).getSelectorData().getConditionList();
+ long queryConditions = conditions.stream()
+ .filter(c -> ParamTypeEnum.QUERY.getName().equals(c.getParamType()))
+ .count();
+ Assertions.assertEquals(1, queryConditions);
+
+ ConditionData queryCondition = conditions.stream()
+ .filter(c -> ParamTypeEnum.QUERY.getName().equals(c.getParamType()))
+ .findFirst().orElse(null);
+ Assertions.assertNotNull(queryCondition);
+ Assertions.assertEquals("debug", queryCondition.getParamName());
+ Assertions.assertEquals("true", queryCondition.getParamValue());
+ }
+
+ /**
+ * Test parse with multiple backend refs.
+ */
+ @Test
+ public void testParseWithMultipleBackendRefs() {
+ Lister endpointsLister = mockEndpointsLister();
+ HttpRouteParser parser = new HttpRouteParser(endpointsLister);
+
+ DynamicKubernetesObject httpRoute = buildHTTPRouteMultiBackend(NAMESPACE, "test-route",
+ NAMESPACE, "shenyu-gateway", SERVICE_NAME, SERVICE_PORT, 8080);
+ ShenyuMemoryConfig config = parser.parse(httpRoute);
+
+ // Should create selector with multiple upstreams
+ List selectors = extractSelectors(config);
+ Assertions.assertEquals(1, selectors.size());
+ }
+
+ private Lister mockEndpointsLister() {
+ Indexer endpointsIndexer = mock(Indexer.class);
+ V1Endpoints mockedEndpoints = new V1EndpointsBuilder().withKind("Endpoints")
+ .withNewMetadata().withNamespace(NAMESPACE).withName(SERVICE_NAME).endMetadata()
+ .withSubsets(new V1EndpointSubsetBuilder()
+ .withAddresses(new V1EndpointAddress().ip("10.0.0.1"), new V1EndpointAddress().ip("10.0.0.2"))
+ .build())
+ .build();
+ when(endpointsIndexer.getByKey(NAMESPACE + "/" + SERVICE_NAME)).thenReturn(mockedEndpoints);
+ return new Lister<>(endpointsIndexer);
+ }
+
+ private List extractSelectors(final ShenyuMemoryConfig config) {
+ return config.getRouteConfigList().stream().map(r -> r.getSelectorData()).toList();
+ }
+
+ private List extractRules(final ShenyuMemoryConfig config) {
+ return config.getRouteConfigList().stream().flatMap(r -> r.getRuleDataList().stream()).toList();
+ }
+
+ private DynamicKubernetesObject buildHTTPRoute(final String routeNamespace, final String routeName,
+ final String gatewayNamespace, final String gatewayName,
+ final String serviceName, final int port,
+ final String pathValue, final String pathType,
+ final String headerName, final String headerValue) {
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("namespace", routeNamespace);
+ metadata.addProperty("name", routeName);
+
+ JsonObject parentRef = new JsonObject();
+ parentRef.addProperty("name", gatewayName);
+ parentRef.addProperty("namespace", gatewayNamespace);
+ JsonArray parentRefs = new JsonArray();
+ parentRefs.add(parentRef);
+
+ JsonObject backendRef = new JsonObject();
+ backendRef.addProperty("name", serviceName);
+ backendRef.addProperty("port", port);
+ JsonArray backendRefs = new JsonArray();
+ backendRefs.add(backendRef);
+
+ JsonObject pathMatch = new JsonObject();
+ pathMatch.addProperty("type", pathType);
+ pathMatch.addProperty("value", pathValue);
+ JsonObject match = new JsonObject();
+ match.add("path", pathMatch);
+
+ if (Objects.nonNull(headerName) && Objects.nonNull(headerValue)) {
+ JsonObject header = new JsonObject();
+ header.addProperty("name", headerName);
+ header.addProperty("value", headerValue);
+ header.addProperty("type", "Exact");
+ JsonArray headers = new JsonArray();
+ headers.add(header);
+ match.add("headers", headers);
+ }
+
+ JsonArray matches = new JsonArray();
+ matches.add(match);
+
+ JsonObject rule = new JsonObject();
+ rule.add("backendRefs", backendRefs);
+ rule.add("matches", matches);
+ JsonArray rules = new JsonArray();
+ rules.add(rule);
+
+ JsonObject spec = new JsonObject();
+ spec.add("parentRefs", parentRefs);
+ spec.add("rules", rules);
+
+ JsonObject raw = new JsonObject();
+ raw.addProperty("apiVersion", "gateway.networking.k8s.io/v1");
+ raw.addProperty("kind", "HTTPRoute");
+ raw.add("metadata", metadata);
+ raw.add("spec", spec);
+ return new DynamicKubernetesObject(raw);
+ }
+
+ private DynamicKubernetesObject buildHTTPRouteWithHostnames(final String routeNamespace, final String routeName,
+ final String gatewayNamespace, final String gatewayName,
+ final String serviceName, final int port,
+ final String pathValue, final String pathType,
+ final String[] hostnames) {
+ DynamicKubernetesObject httpRoute = buildHTTPRoute(routeNamespace, routeName,
+ gatewayNamespace, gatewayName, serviceName, port, pathValue, pathType, null, null);
+
+ JsonArray hostnameArray = new JsonArray();
+ for (String hostname : hostnames) {
+ hostnameArray.add(hostname);
+ }
+ httpRoute.getRaw().getAsJsonObject("spec").add("hostnames", hostnameArray);
+ return httpRoute;
+ }
+
+ private DynamicKubernetesObject buildHTTPRouteWithQueryParams(final String routeNamespace, final String routeName,
+ final String gatewayNamespace, final String gatewayName,
+ final String serviceName, final int port,
+ final String pathValue, final String pathType,
+ final String queryName, final String queryValue,
+ final String queryType) {
+ // Add query params to the match
+ JsonObject queryParam = new JsonObject();
+ queryParam.addProperty("name", queryName);
+ queryParam.addProperty("value", queryValue);
+ queryParam.addProperty("type", queryType);
+ JsonArray queryParams = new JsonArray();
+ queryParams.add(queryParam);
+
+ final DynamicKubernetesObject httpRoute = buildHTTPRoute(routeNamespace, routeName,
+ gatewayNamespace, gatewayName, serviceName, port, pathValue, pathType, null, null);
+ JsonArray rules = httpRoute.getRaw().getAsJsonObject("spec").getAsJsonArray("rules");
+ JsonObject firstRule = rules.get(0).getAsJsonObject();
+ JsonObject firstMatch = firstRule.getAsJsonArray("matches").get(0).getAsJsonObject();
+ firstMatch.add("queryParams", queryParams);
+
+ return httpRoute;
+ }
+
+ private DynamicKubernetesObject buildHTTPRouteMultiBackend(final String routeNamespace, final String routeName,
+ final String gatewayNamespace, final String gatewayName,
+ final String serviceName, final int port1, final int port2) {
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("namespace", routeNamespace);
+ metadata.addProperty("name", routeName);
+
+ JsonObject parentRef = new JsonObject();
+ parentRef.addProperty("name", gatewayName);
+ parentRef.addProperty("namespace", gatewayNamespace);
+ JsonArray parentRefs = new JsonArray();
+ parentRefs.add(parentRef);
+
+ JsonObject backendRef1 = new JsonObject();
+ backendRef1.addProperty("name", serviceName);
+ backendRef1.addProperty("port", port1);
+ backendRef1.addProperty("weight", 70);
+
+ JsonObject backendRef2 = new JsonObject();
+ backendRef2.addProperty("name", serviceName);
+ backendRef2.addProperty("port", port2);
+ backendRef2.addProperty("weight", 30);
+
+ JsonArray backendRefs = new JsonArray();
+ backendRefs.add(backendRef1);
+ backendRefs.add(backendRef2);
+
+ JsonObject pathMatch = new JsonObject();
+ pathMatch.addProperty("type", "PathPrefix");
+ pathMatch.addProperty("value", "/**");
+ JsonObject match = new JsonObject();
+ match.add("path", pathMatch);
+ JsonArray matches = new JsonArray();
+ matches.add(match);
+
+ JsonObject rule = new JsonObject();
+ rule.add("backendRefs", backendRefs);
+ rule.add("matches", matches);
+ JsonArray rules = new JsonArray();
+ rules.add(rule);
+
+ JsonObject spec = new JsonObject();
+ spec.add("parentRefs", parentRefs);
+ spec.add("rules", rules);
+
+ JsonObject raw = new JsonObject();
+ raw.addProperty("apiVersion", "gateway.networking.k8s.io/v1");
+ raw.addProperty("kind", "HTTPRoute");
+ raw.add("metadata", metadata);
+ raw.add("spec", spec);
+ return new DynamicKubernetesObject(raw);
+ }
+
+ private DynamicKubernetesObject buildHTTPRouteNoRules(final String routeNamespace, final String routeName) {
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("namespace", routeNamespace);
+ metadata.addProperty("name", routeName);
+
+ JsonObject spec = new JsonObject();
+
+ JsonObject raw = new JsonObject();
+ raw.addProperty("apiVersion", "gateway.networking.k8s.io/v1");
+ raw.addProperty("kind", "HTTPRoute");
+ raw.add("metadata", metadata);
+ raw.add("spec", spec);
+ return new DynamicKubernetesObject(raw);
+ }
+}
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java
new file mode 100644
index 000000000000..4e238285e3a8
--- /dev/null
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.springboot.starter.k8s;
+
+import io.kubernetes.client.extended.controller.Controller;
+import io.kubernetes.client.extended.controller.ControllerManager;
+import io.kubernetes.client.extended.controller.DefaultController;
+import io.kubernetes.client.extended.controller.builder.ControllerBuilder;
+import io.kubernetes.client.extended.controller.builder.DefaultControllerBuilder;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.workqueue.RateLimitingQueue;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.SharedInformerFactory;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1EndpointsList;
+import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.enums.PluginRoleEnum;
+import org.apache.shenyu.k8s.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.parser.HttpRouteParser;
+import org.apache.shenyu.k8s.reconciler.GatewayClassReconciler;
+import org.apache.shenyu.k8s.reconciler.GatewayReconciler;
+import org.apache.shenyu.k8s.reconciler.HTTPRouteReconciler;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.apache.shenyu.plugin.base.cache.CommonDiscoveryUpstreamDataSubscriber;
+import org.apache.shenyu.plugin.base.cache.CommonPluginDataSubscriber;
+import org.apache.shenyu.plugin.global.subsciber.MetaDataCacheSubscriber;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@Configuration
+@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "gateway-api")
+public class GatewayApiControllerConfiguration {
+
+ /**
+ * GatewayClass SharedInformerFactory - only registers GatewayClass informer.
+ * Separate factory to avoid DynamicKubernetesObject class key collision.
+ *
+ * @param apiClient the Kubernetes API client
+ * @return the SharedInformerFactory for GatewayClass resources
+ */
+ @Bean("gatewayclass-shared-informer-factory")
+ public SharedInformerFactory gatewayClassSharedInformerFactory(final ApiClient apiClient) {
+ SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+ DynamicKubernetesApi gatewayClassApi = new DynamicKubernetesApi(
+ GatewayApiConstants.GATEWAY_API_GROUP,
+ GatewayApiConstants.GATEWAY_API_VERSION,
+ "gatewayclasses",
+ apiClient);
+ factory.sharedIndexInformerFor(gatewayClassApi, DynamicKubernetesObject.class, 0);
+ return factory;
+ }
+
+ /**
+ * Gateway SharedInformerFactory - only registers Gateway informer.
+ * Separate from other factories to avoid DynamicKubernetesObject class key collision.
+ *
+ * @param apiClient the Kubernetes API client
+ * @return the SharedInformerFactory for Gateway resources
+ */
+ @Bean("gateway-shared-informer-factory")
+ public SharedInformerFactory gatewaySharedInformerFactory(final ApiClient apiClient) {
+ SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+ DynamicKubernetesApi gatewayApi = new DynamicKubernetesApi(
+ GatewayApiConstants.GATEWAY_API_GROUP,
+ GatewayApiConstants.GATEWAY_API_VERSION,
+ "gateways",
+ apiClient);
+ factory.sharedIndexInformerFor(gatewayApi, DynamicKubernetesObject.class, 0);
+ return factory;
+ }
+
+ /**
+ * HTTPRoute SharedInformerFactory - registers HTTPRoute and Endpoints informers.
+ * Separate from gatewayFactory to avoid DynamicKubernetesObject class key collision.
+ *
+ * @param apiClient the Kubernetes API client
+ * @return the SharedInformerFactory for HTTPRoute and Endpoints resources
+ */
+ @Bean("httproute-shared-informer-factory")
+ public SharedInformerFactory httpRouteSharedInformerFactory(final ApiClient apiClient) {
+ SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+ DynamicKubernetesApi httpRouteApi = new DynamicKubernetesApi(
+ GatewayApiConstants.GATEWAY_API_GROUP,
+ GatewayApiConstants.GATEWAY_API_VERSION,
+ "httproutes",
+ apiClient);
+ factory.sharedIndexInformerFor(httpRouteApi, DynamicKubernetesObject.class, 0);
+
+ GenericKubernetesApi endpointsApi = new GenericKubernetesApi<>(V1Endpoints.class,
+ V1EndpointsList.class, "", "v1", "endpoints", apiClient);
+ factory.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0);
+ return factory;
+ }
+
+ /**
+ * Shared ExecutorService for all ControllerManager beans, with a destroy method to
+ * ensure graceful shutdown and prevent thread leaks on context close.
+ *
+ * @return daemon cached thread pool executor
+ */
+ @Bean(destroyMethod = "shutdown")
+ public ExecutorService controllerExecutorService() {
+ return Executors.newCachedThreadPool(r -> {
+ Thread t = new Thread(r, "shenyu-k8s-controller");
+ t.setDaemon(true);
+ return t;
+ });
+ }
+
+ @Bean("gatewayclass-controller-manager")
+ public ControllerManager gatewayClassControllerManager(
+ @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory,
+ @Qualifier("gatewayclass-controller") final Controller gatewayClassController,
+ final ExecutorService controllerExecutorService) {
+ ControllerManager controllerManager = new ControllerManager(gatewayClassFactory, gatewayClassController);
+ controllerExecutorService.submit(controllerManager);
+ return controllerManager;
+ }
+
+ @Bean("gateway-controller-manager")
+ public ControllerManager gatewayControllerManager(
+ @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory,
+ @Qualifier("gateway-controller") final Controller gatewayController,
+ final ExecutorService controllerExecutorService) {
+ ControllerManager controllerManager = new ControllerManager(gatewayFactory, gatewayController);
+ controllerExecutorService.submit(controllerManager);
+ return controllerManager;
+ }
+
+ @Bean("httproute-controller-manager")
+ public ControllerManager httpRouteControllerManager(
+ @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory,
+ @Qualifier("httproute-controller") final Controller httpRouteController,
+ final ExecutorService controllerExecutorService) {
+ ControllerManager controllerManager = new ControllerManager(httpRouteFactory, httpRouteController);
+ controllerExecutorService.submit(controllerManager);
+ return controllerManager;
+ }
+
+ @Bean("gatewayclass-controller")
+ public Controller gatewayClassController(
+ @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory,
+ final GatewayClassReconciler gatewayClassReconciler) {
+ DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(gatewayClassFactory);
+ builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(DynamicKubernetesObject.class, q)
+ .withResyncPeriod(Duration.ofMinutes(1))
+ .build());
+ builder.withWorkerCount(1);
+ return builder.withReconciler(gatewayClassReconciler).withName("gatewayClassController").build();
+ }
+
+ @Bean("gateway-controller")
+ public Controller gatewayController(
+ @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory,
+ final GatewayReconciler gatewayReconciler) {
+ DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(gatewayFactory);
+ builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(DynamicKubernetesObject.class, q)
+ .withResyncPeriod(Duration.ofMinutes(1))
+ .build());
+ builder.withWorkerCount(2);
+ return builder.withReconciler(gatewayReconciler).withName("gatewayController").build();
+ }
+
+ @Bean("httproute-controller")
+ public Controller httpRouteController(
+ @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory,
+ final HTTPRouteReconciler httpRouteReconciler) {
+ DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(httpRouteFactory);
+ builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(DynamicKubernetesObject.class, q)
+ .withResyncPeriod(Duration.ofMinutes(1))
+ .build());
+ builder.withWorkerCount(2);
+ return builder.withReconciler(httpRouteReconciler).withName("httpRouteController").build();
+ }
+
+ @Bean
+ public GatewayClassReconciler gatewayClassReconciler(
+ @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory,
+ @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory,
+ @Qualifier("gateway-controller") final Controller gatewayController,
+ final ApiClient apiClient) {
+ SharedIndexInformer gatewayClassInformer =
+ gatewayClassFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+ SharedIndexInformer gatewayInformer =
+ gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+ RateLimitingQueue gatewayWorkQueue = ((DefaultController) gatewayController).getWorkQueue();
+ return new GatewayClassReconciler(gatewayClassInformer, gatewayInformer, gatewayWorkQueue, apiClient);
+ }
+
+ @Bean
+ public GatewayReconciler gatewayReconciler(
+ @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory,
+ @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory,
+ @Qualifier("httproute-controller") final Controller httpRouteController,
+ final ShenyuCacheRepository shenyuCacheRepository,
+ final ApiClient apiClient) {
+ SharedIndexInformer gatewayInformer =
+ gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+ SharedIndexInformer httpRouteInformer =
+ httpRouteFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+ RateLimitingQueue httpRouteWorkQueue = ((DefaultController) httpRouteController).getWorkQueue();
+ return new GatewayReconciler(gatewayInformer, httpRouteInformer, shenyuCacheRepository, httpRouteWorkQueue, apiClient);
+ }
+
+ @Bean
+ public HTTPRouteReconciler httpRouteReconciler(
+ @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory,
+ @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory,
+ final HttpRouteParser httpRouteParser,
+ final ShenyuCacheRepository shenyuCacheRepository,
+ final ApiClient apiClient) {
+ SharedIndexInformer httpRouteInformer =
+ httpRouteFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+ SharedIndexInformer gatewayInformer =
+ gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+ return new HTTPRouteReconciler(httpRouteInformer, gatewayInformer, httpRouteParser, shenyuCacheRepository, apiClient);
+ }
+
+ @Bean
+ public HttpRouteParser httpRouteParser(
+ @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory) {
+ SharedIndexInformer endpointsInformer =
+ httpRouteFactory.getExistingSharedIndexInformer(V1Endpoints.class);
+ Lister endpointsLister = new Lister<>(endpointsInformer.getIndexer());
+ return new HttpRouteParser(endpointsLister);
+ }
+
+ @Bean
+ public ShenyuCacheRepository shenyuCacheRepository(final CommonPluginDataSubscriber pluginDataSubscriber,
+ final CommonDiscoveryUpstreamDataSubscriber discoveryUpstreamDataSubscriber,
+ final MetaDataCacheSubscriber metaDataSubscriber,
+ final MetaDataCacheSubscriber metaDataCacheSubscriber) {
+ ShenyuCacheRepository repository = new ShenyuCacheRepository(pluginDataSubscriber, discoveryUpstreamDataSubscriber, metaDataSubscriber, metaDataCacheSubscriber);
+ enablePlugin(repository, PluginEnum.GLOBAL, null);
+ enablePlugin(repository, PluginEnum.URI, null);
+ enablePlugin(repository, PluginEnum.NETTY_HTTP_CLIENT, null);
+ enablePlugin(repository, PluginEnum.DIVIDE, "{multiSelectorHandle: 1, multiRuleHandle:0}");
+ enablePlugin(repository, PluginEnum.GENERAL_CONTEXT, null);
+ return repository;
+ }
+
+ private void enablePlugin(final ShenyuCacheRepository shenyuCacheRepository, final PluginEnum pluginEnum, final String config) {
+ PluginData pluginData = PluginData.builder()
+ .id(String.valueOf(pluginEnum.getCode()))
+ .name(pluginEnum.getName())
+ .config(config)
+ .role(PluginRoleEnum.SYS.getName())
+ .enabled(true)
+ .sort(pluginEnum.getCode())
+ .build();
+ shenyuCacheRepository.saveOrUpdatePluginData(pluginData);
+ }
+}
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
index 0a6ad99abd1e..e9ac6317ee47 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
@@ -66,6 +66,7 @@
* The type shenyu ingress controller configuration.
*/
@Configuration
+@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "ingress", matchIfMissing = true)
public class IngressControllerConfiguration {
/**
@@ -264,7 +265,7 @@ public TcpSslContextSpec tcpSslContextSpec(final ObjectProvider secretData = secret.getData();
- if (MapUtils.isEmpty(secretData)) {
+ if (MapUtils.isNotEmpty(secretData)) {
InputStream crtStream = new ByteArrayInputStream(secretData.get("tls.crt"));
InputStream keyStream = new ByteArrayInputStream(secretData.get("tls.key"));
return TcpSslContextSpec.forServer(crtStream, keyStream);
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring.factories b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring.factories
index 6899440f01ba..a386224c21f6 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring.factories
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring.factories
@@ -1,3 +1,4 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-org.apache.shenyu.springboot.starter.k8s.IngressControllerConfiguration
+org.apache.shenyu.springboot.starter.k8s.IngressControllerConfiguration,\
+org.apache.shenyu.springboot.starter.k8s.GatewayApiControllerConfiguration
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 3eb3d33916e9..b641d56b7013 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -16,3 +16,4 @@
#
org.apache.shenyu.springboot.starter.k8s.IngressControllerConfiguration
+org.apache.shenyu.springboot.starter.k8s.GatewayApiControllerConfiguration