Skip to content

Commit 27d22eb

Browse files
committed
Migrate xDS resource services from gRPC to YAML-over-HTTP annotated services
Motivation: The xDS resource management APIs (cluster, endpoint, listener, route, group, kubernetes) were implemented as gRPC services backed by service-specific `.proto` files. Migrating them to Armeria annotated HTTP services that consume `application/yaml` removes the protobuf service layer, simplifies the API surface, and makes the YAML-native storage format a first-class concern — clients send YAML directly and it is stored as-is (with `name`/`clusterName` injected). Modifications: - Convert `XdsClusterService`, `XdsEndpointService`, `XdsListenerService`, `XdsRouteService` from `*ImplBase` gRPC stubs to plain annotated HTTP services (`@Post`/`@Put`/`@Delete`, `@Consumes("application/yaml")`). - Add `RequiresXdsGroupRole` annotation and `RequiresXdsGroupRoleDecorator` to replace the inline `checkWritePermission` calls with a declarative, per-method authorization mechanism. - Refactor `XdsResourceManager` so that `push`/`update`/`delete` operate on raw YAML strings instead of protobuf messages, and add helpers `parseYaml`, `normalizeYamlKeys`, `injectYamlField`, and `errorResponse`. Result: - xDS resource CRUD is now served via plain HTTP with YAML bodies, removing the gRPC service layer and the associated proto wrapper definitions.
1 parent 5bc736b commit 27d22eb

35 files changed

Lines changed: 1717 additions & 1691 deletions

it/xds-k8s-node-ip-extractor/src/test/java/com/linecorp/centraldogma/it/xds/k8s/XdsKubernetesNodeIpExtractorTest.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import com.linecorp.centraldogma.common.Entry;
4444
import com.linecorp.centraldogma.common.Query;
4545
import com.linecorp.centraldogma.common.Revision;
46+
import com.linecorp.centraldogma.internal.Jackson;
47+
import com.linecorp.centraldogma.internal.Yaml;
4648
import com.linecorp.centraldogma.server.storage.repository.Repository;
4749
import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension;
4850
import com.linecorp.centraldogma.xds.internal.XdsResourceManager;
@@ -147,7 +149,6 @@ void extractsNodeIpFromLabel() throws Exception {
147149

148150
final AggregatedHttpResponse response = createAggregator(aggregator, aggregatorId);
149151
assertThat(response.status()).isSameAs(HttpStatus.OK);
150-
assertThat(response.headers().get("grpc-status")).isEqualTo("0");
151152

152153
final Repository fooGroup = dogma.projectManager().get(INTERNAL_PROJECT_XDS)
153154
.repos().get("foo");
@@ -223,7 +224,6 @@ void fallsBackToInternalIpWhenLabelKeyIsAbsent() throws Exception {
223224

224225
final AggregatedHttpResponse response = createAggregator(aggregator, aggregatorId);
225226
assertThat(response.status()).isSameAs(HttpStatus.OK);
226-
assertThat(response.headers().get("grpc-status")).isEqualTo("0");
227227

228228
final Repository fooGroup = dogma.projectManager().get(INTERNAL_PROJECT_XDS)
229229
.repos().get("foo");
@@ -315,11 +315,12 @@ private static AggregatedHttpResponse createAggregator(
315315
RequestHeaders.builder(HttpMethod.POST,
316316
"/api/v1/xds/groups/foo/k8s/endpointAggregators?" +
317317
"aggregator_id=" + aggregatorId)
318-
.contentType(MediaType.JSON_UTF_8)
318+
.contentType(MediaType.parse("application/yaml"))
319319
.set(HttpHeaderNames.AUTHORIZATION, "Bearer anonymous")
320320
.build();
321-
return dogma.httpClient().blocking().execute(
322-
headers, XdsResourceManager.JSON_MESSAGE_MARSHALLER.writeValueAsString(aggregator));
321+
final String yaml = Yaml.writeValueAsString(
322+
Jackson.readTree(XdsResourceManager.JSON_MESSAGE_MARSHALLER.writeValueAsString(aggregator)));
323+
return dogma.httpClient().blocking().execute(headers, yaml);
323324
}
324325

325326
private static Node newNodeWithLabel(String internalIp, String labelKey, String labelValue) {

webapp/src/dogma/features/xds/XdsTypes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ export const XDS_RESOURCE_TEMPLATES: Record<XdsResourceType, string> = {
125125
},
126126
},
127127
],
128-
respectDnsTtl: true,
129128
},
130129
null,
131130
2,

xds/src/main/java/com/linecorp/centraldogma/xds/cluster/v1/XdsClusterService.java

Lines changed: 91 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,30 @@
2020
import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.CLUSTERS_DIRECTORY;
2121
import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.LEGACY_RESOURCE_ID_PATTERN_STRING;
2222
import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.RESOURCE_ID_PATTERN;
23-
import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.removePrefix;
2423

25-
import java.util.regex.Matcher;
24+
import java.io.IOException;
25+
import java.util.concurrent.CompletableFuture;
2626
import java.util.regex.Pattern;
2727

28-
import com.google.protobuf.Empty;
28+
import org.jspecify.annotations.Nullable;
2929

30-
import com.linecorp.centraldogma.xds.cluster.v1.XdsClusterServiceGrpc.XdsClusterServiceImplBase;
30+
import com.linecorp.armeria.common.HttpResponse;
31+
import com.linecorp.armeria.common.HttpStatus;
32+
import com.linecorp.armeria.server.annotation.Consumes;
33+
import com.linecorp.armeria.server.annotation.Delete;
34+
import com.linecorp.armeria.server.annotation.Param;
35+
import com.linecorp.armeria.server.annotation.Post;
36+
import com.linecorp.armeria.server.annotation.Put;
37+
import com.linecorp.centraldogma.common.RepositoryRole;
38+
import com.linecorp.centraldogma.xds.internal.RequiresXdsGroupRole;
3139
import com.linecorp.centraldogma.xds.internal.XdsResourceManager;
3240

3341
import io.envoyproxy.envoy.config.cluster.v3.Cluster;
34-
import io.grpc.Status;
35-
import io.grpc.stub.StreamObserver;
3642

3743
/**
38-
* Service for managing clusters.
44+
* Annotated service object for managing clusters.
3945
*/
40-
public final class XdsClusterService extends XdsClusterServiceImplBase {
46+
public final class XdsClusterService {
4147

4248
private static final Pattern CLUSTER_NAME_PATTERN =
4349
Pattern.compile("^groups/([^/]+)/clusters/" + LEGACY_RESOURCE_ID_PATTERN_STRING + '$');
@@ -51,69 +57,89 @@ public XdsClusterService(XdsResourceManager xdsResourceManager) {
5157
this.xdsResourceManager = xdsResourceManager;
5258
}
5359

54-
@Override
55-
public void createCluster(CreateClusterRequest request, StreamObserver<Cluster> responseObserver) {
56-
final String parent = request.getParent();
57-
final String group = removePrefix("groups/", parent);
58-
xdsResourceManager.checkWritePermission(group);
59-
60-
final String clusterId = request.getClusterId();
60+
/**
61+
* POST /xds/groups/{group}/clusters
62+
*
63+
* <p>Creates a new cluster.
64+
*/
65+
@Post("/xds/groups/{group}/clusters")
66+
@Consumes("application/yaml")
67+
@RequiresXdsGroupRole(RepositoryRole.WRITE)
68+
public CompletableFuture<HttpResponse> createCluster(
69+
@Param("group") String group,
70+
@Param("cluster_id") String clusterId,
71+
@Param("summary") @Nullable String summary,
72+
String body) {
6173
if (!RESOURCE_ID_PATTERN.matcher(clusterId).matches()) {
62-
throw Status.INVALID_ARGUMENT.withDescription("Invalid cluster_id: " + clusterId +
63-
" (expected: " + RESOURCE_ID_PATTERN + ')')
64-
.asRuntimeException();
74+
return CompletableFuture.completedFuture(
75+
XdsResourceManager.errorResponse(HttpStatus.BAD_REQUEST,
76+
"Invalid cluster ID: " + clusterId));
6577
}
66-
67-
final String clusterName = parent + CLUSTERS_DIRECTORY + clusterId;
68-
final Cluster cluster
69-
= request.getCluster()
70-
.toBuilder()
71-
// Ignore the specified name in the cluster and set the name with the format of
72-
// "groups/{group}/clusters/{cluster}".
73-
// https://github.com/aip-dev/google.aip.dev/blob/master/aip/general/0133.md#user-specified-ids
74-
.setName(clusterName)
75-
// Respect the DNS TTL would be more efficient in terms of DNS resolution.
76-
// https://github.com/envoyproxy/envoy/issues/6876
77-
// `respect_dns_ttl` is a `bool` field so it is not possible to check whether a value
78-
// has not been set for the field. Until we create our own proto file, the value only
79-
// can be set to false via the update API.
80-
.setRespectDnsTtl(true)
81-
.build();
82-
final String createSummary = isNullOrEmpty(request.getSummary()) ?
83-
"Create cluster: " + clusterName : request.getSummary();
84-
xdsResourceManager.push(responseObserver, group, clusterName, CLUSTERS_DIRECTORY + clusterId + ".yaml",
85-
createSummary, cluster, currentAuthor(), true);
86-
}
87-
88-
@Override
89-
public void updateCluster(UpdateClusterRequest request, StreamObserver<Cluster> responseObserver) {
90-
final Cluster cluster = request.getCluster();
91-
final String clusterName = cluster.getName();
92-
final String group = checkClusterName(clusterName).group(1);
93-
xdsResourceManager.checkWritePermission(group);
94-
final String updateSummary = isNullOrEmpty(request.getSummary()) ?
95-
"Update cluster: " + clusterName : request.getSummary();
96-
xdsResourceManager.update(responseObserver, group, clusterName,
97-
updateSummary, cluster, currentAuthor());
78+
final String clusterName = "groups/" + group + CLUSTERS_DIRECTORY + clusterId;
79+
try {
80+
XdsResourceManager.parseYaml(body, Cluster.newBuilder());
81+
} catch (IOException e) {
82+
return CompletableFuture.completedFuture(
83+
XdsResourceManager.errorResponse(HttpStatus.BAD_REQUEST,
84+
"Invalid request body: " + e.getMessage()));
85+
}
86+
final String createSummary = isNullOrEmpty(summary) ? "Create cluster: " + clusterName : summary;
87+
final String normalizedBody = XdsResourceManager.normalizeYamlKeys(body);
88+
final String bodyToStore = XdsResourceManager.injectYamlField(normalizedBody, "name", clusterName);
89+
return xdsResourceManager.push(group, clusterName, CLUSTERS_DIRECTORY + clusterId + ".yaml",
90+
createSummary, currentAuthor(), true, bodyToStore);
9891
}
9992

100-
@Override
101-
public void deleteCluster(DeleteClusterRequest request, StreamObserver<Empty> responseObserver) {
102-
final String clusterName = request.getName();
103-
final String group = checkClusterName(clusterName).group(1);
104-
xdsResourceManager.checkWritePermission(group);
105-
final String deleteSummary = isNullOrEmpty(request.getSummary()) ?
106-
"Delete cluster: " + clusterName : request.getSummary();
107-
xdsResourceManager.delete(responseObserver, group, clusterName, deleteSummary, currentAuthor());
93+
/**
94+
* PUT /xds/groups/{group}/clusters/{cluster_id}
95+
*
96+
* <p>Updates an existing cluster.
97+
*/
98+
@Put("/xds/groups/{group}/clusters/{*cluster_id}")
99+
@Consumes("application/yaml")
100+
@RequiresXdsGroupRole(RepositoryRole.WRITE)
101+
public CompletableFuture<HttpResponse> updateCluster(
102+
@Param("group") String group,
103+
@Param("cluster_id") String clusterId,
104+
@Param("summary") @Nullable String summary,
105+
String body) {
106+
final String clusterName = "groups/" + group + "/clusters/" + clusterId;
107+
if (!CLUSTER_NAME_PATTERN.matcher(clusterName).matches()) {
108+
return CompletableFuture.completedFuture(
109+
XdsResourceManager.errorResponse(HttpStatus.BAD_REQUEST,
110+
"Invalid cluster name: " + clusterName));
111+
}
112+
try {
113+
XdsResourceManager.parseYaml(body, Cluster.newBuilder());
114+
} catch (IOException e) {
115+
return CompletableFuture.completedFuture(
116+
XdsResourceManager.errorResponse(HttpStatus.BAD_REQUEST,
117+
"Invalid request body: " + e.getMessage()));
118+
}
119+
final String updateSummary = isNullOrEmpty(summary) ? "Update cluster: " + clusterName : summary;
120+
final String normalizedBody = XdsResourceManager.normalizeYamlKeys(body);
121+
final String bodyToStore = XdsResourceManager.injectYamlField(normalizedBody, "name", clusterName);
122+
return xdsResourceManager.update(group, clusterName, updateSummary, currentAuthor(), bodyToStore);
108123
}
109124

110-
private static Matcher checkClusterName(String clusterName) {
111-
final Matcher matcher = CLUSTER_NAME_PATTERN.matcher(clusterName);
112-
if (!matcher.matches()) {
113-
throw Status.INVALID_ARGUMENT.withDescription("Invalid cluster name: " + clusterName +
114-
" (expected: " + CLUSTER_NAME_PATTERN + ')')
115-
.asRuntimeException();
125+
/**
126+
* DELETE /xds/groups/{group}/clusters/{cluster_id}
127+
*
128+
* <p>Removes a cluster.
129+
*/
130+
@Delete("/xds/groups/{group}/clusters/{*cluster_id}")
131+
@RequiresXdsGroupRole(RepositoryRole.WRITE)
132+
public CompletableFuture<HttpResponse> deleteCluster(
133+
@Param("group") String group,
134+
@Param("cluster_id") String clusterId,
135+
@Param("summary") @Nullable String summary) {
136+
final String clusterName = "groups/" + group + "/clusters/" + clusterId;
137+
if (!CLUSTER_NAME_PATTERN.matcher(clusterName).matches()) {
138+
return CompletableFuture.completedFuture(
139+
XdsResourceManager.errorResponse(HttpStatus.BAD_REQUEST,
140+
"Invalid cluster name: " + clusterName));
116141
}
117-
return matcher;
142+
final String deleteSummary = isNullOrEmpty(summary) ? "Delete cluster: " + clusterName : summary;
143+
return xdsResourceManager.delete(group, clusterName, deleteSummary, currentAuthor());
118144
}
119145
}

0 commit comments

Comments
 (0)