-
Notifications
You must be signed in to change notification settings - Fork 134
Migrate xDS resource services from gRPC to YAML-over-HTTP annotated services #1337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
27d22eb
509a416
741077d
18ce744
5cecafd
ebe1fb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,24 +20,30 @@ | |
| import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.CLUSTERS_DIRECTORY; | ||
| import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.LEGACY_RESOURCE_ID_PATTERN_STRING; | ||
| import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.RESOURCE_ID_PATTERN; | ||
| import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.removePrefix; | ||
|
|
||
| import java.util.regex.Matcher; | ||
| import java.io.IOException; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| import com.google.protobuf.Empty; | ||
| import org.jspecify.annotations.Nullable; | ||
|
|
||
| import com.linecorp.centraldogma.xds.cluster.v1.XdsClusterServiceGrpc.XdsClusterServiceImplBase; | ||
| import com.linecorp.armeria.common.HttpResponse; | ||
| import com.linecorp.armeria.common.HttpStatus; | ||
| import com.linecorp.armeria.server.annotation.Consumes; | ||
| import com.linecorp.armeria.server.annotation.Delete; | ||
| import com.linecorp.armeria.server.annotation.Param; | ||
| import com.linecorp.armeria.server.annotation.Post; | ||
| import com.linecorp.armeria.server.annotation.Put; | ||
| import com.linecorp.centraldogma.common.RepositoryRole; | ||
| import com.linecorp.centraldogma.xds.internal.RequiresXdsGroupRole; | ||
| import com.linecorp.centraldogma.xds.internal.XdsResourceManager; | ||
|
|
||
| import io.envoyproxy.envoy.config.cluster.v3.Cluster; | ||
| import io.grpc.Status; | ||
| import io.grpc.stub.StreamObserver; | ||
|
|
||
| /** | ||
| * Service for managing clusters. | ||
| * Annotated service object for managing clusters. | ||
| */ | ||
| public final class XdsClusterService extends XdsClusterServiceImplBase { | ||
| public final class XdsClusterService { | ||
|
|
||
| private static final Pattern CLUSTER_NAME_PATTERN = | ||
| Pattern.compile("^groups/([^/]+)/clusters/" + LEGACY_RESOURCE_ID_PATTERN_STRING + '$'); | ||
|
|
@@ -51,69 +57,87 @@ public XdsClusterService(XdsResourceManager xdsResourceManager) { | |
| this.xdsResourceManager = xdsResourceManager; | ||
| } | ||
|
|
||
| @Override | ||
| public void createCluster(CreateClusterRequest request, StreamObserver<Cluster> responseObserver) { | ||
| final String parent = request.getParent(); | ||
| final String group = removePrefix("groups/", parent); | ||
| xdsResourceManager.checkWritePermission(group); | ||
|
|
||
| final String clusterId = request.getClusterId(); | ||
| /** | ||
| * POST /xds/groups/{group}/clusters | ||
| * | ||
| * <p>Creates a new cluster. | ||
| */ | ||
| @Post("/xds/groups/{group}/clusters") | ||
| @Consumes("application/yaml") | ||
| @RequiresXdsGroupRole(RepositoryRole.WRITE) | ||
| public CompletableFuture<HttpResponse> createCluster( | ||
| @Param("group") String group, | ||
| @Param("cluster_id") String clusterId, | ||
| @Param("summary") @Nullable String summary, | ||
| String body) { | ||
| if (!RESOURCE_ID_PATTERN.matcher(clusterId).matches()) { | ||
| throw Status.INVALID_ARGUMENT.withDescription("Invalid cluster_id: " + clusterId + | ||
| " (expected: " + RESOURCE_ID_PATTERN + ')') | ||
| .asRuntimeException(); | ||
| return CompletableFuture.completedFuture( | ||
| XdsResourceManager.errorResponse(HttpStatus.BAD_REQUEST, | ||
| "Invalid cluster ID: " + clusterId)); | ||
| } | ||
|
|
||
| final String clusterName = parent + CLUSTERS_DIRECTORY + clusterId; | ||
| final Cluster cluster | ||
| = request.getCluster() | ||
| .toBuilder() | ||
| // Ignore the specified name in the cluster and set the name with the format of | ||
| // "groups/{group}/clusters/{cluster}". | ||
| // https://github.com/aip-dev/google.aip.dev/blob/master/aip/general/0133.md#user-specified-ids | ||
| .setName(clusterName) | ||
| // Respect the DNS TTL would be more efficient in terms of DNS resolution. | ||
| // https://github.com/envoyproxy/envoy/issues/6876 | ||
| // `respect_dns_ttl` is a `bool` field so it is not possible to check whether a value | ||
| // has not been set for the field. Until we create our own proto file, the value only | ||
| // can be set to false via the update API. | ||
| .setRespectDnsTtl(true) | ||
| .build(); | ||
| final String createSummary = isNullOrEmpty(request.getSummary()) ? | ||
| "Create cluster: " + clusterName : request.getSummary(); | ||
| xdsResourceManager.push(responseObserver, group, clusterName, CLUSTERS_DIRECTORY + clusterId + ".yaml", | ||
| createSummary, cluster, currentAuthor(), true); | ||
| } | ||
|
|
||
| @Override | ||
| public void updateCluster(UpdateClusterRequest request, StreamObserver<Cluster> responseObserver) { | ||
| final Cluster cluster = request.getCluster(); | ||
| final String clusterName = cluster.getName(); | ||
| final String group = checkClusterName(clusterName).group(1); | ||
| xdsResourceManager.checkWritePermission(group); | ||
| final String updateSummary = isNullOrEmpty(request.getSummary()) ? | ||
| "Update cluster: " + clusterName : request.getSummary(); | ||
| xdsResourceManager.update(responseObserver, group, clusterName, | ||
| updateSummary, cluster, currentAuthor()); | ||
| final String clusterName = "groups/" + group + CLUSTERS_DIRECTORY + clusterId; | ||
| try { | ||
| XdsResourceManager.parseYaml(body, Cluster.newBuilder()); | ||
| } catch (IOException e) { | ||
| return CompletableFuture.completedFuture( | ||
| XdsResourceManager.errorResponse(HttpStatus.BAD_REQUEST, | ||
| "Invalid request body: " + e.getMessage())); | ||
| } | ||
| final String createSummary = isNullOrEmpty(summary) ? "Create cluster: " + clusterName : summary; | ||
| final String bodyToStore = XdsResourceManager.injectYamlField(body, "name", clusterName); | ||
| return xdsResourceManager.push(group, clusterName, CLUSTERS_DIRECTORY + clusterId + ".yaml", | ||
| createSummary, currentAuthor(), true, bodyToStore); | ||
| } | ||
|
|
||
| @Override | ||
| public void deleteCluster(DeleteClusterRequest request, StreamObserver<Empty> responseObserver) { | ||
| final String clusterName = request.getName(); | ||
| final String group = checkClusterName(clusterName).group(1); | ||
| xdsResourceManager.checkWritePermission(group); | ||
| final String deleteSummary = isNullOrEmpty(request.getSummary()) ? | ||
| "Delete cluster: " + clusterName : request.getSummary(); | ||
| xdsResourceManager.delete(responseObserver, group, clusterName, deleteSummary, currentAuthor()); | ||
| /** | ||
| * PUT /xds/groups/{group}/clusters/{cluster_id} | ||
| * | ||
| * <p>Updates an existing cluster. | ||
| */ | ||
| @Put("/xds/groups/{group}/clusters/{*cluster_id}") | ||
| @Consumes("application/yaml") | ||
| @RequiresXdsGroupRole(RepositoryRole.WRITE) | ||
| public CompletableFuture<HttpResponse> updateCluster( | ||
| @Param("group") String group, | ||
| @Param("cluster_id") String clusterId, | ||
| @Param("summary") @Nullable String summary, | ||
| String body) { | ||
| final String clusterName = "groups/" + group + "/clusters/" + clusterId; | ||
| if (!CLUSTER_NAME_PATTERN.matcher(clusterName).matches()) { | ||
| return CompletableFuture.completedFuture( | ||
| XdsResourceManager.errorResponse(HttpStatus.BAD_REQUEST, | ||
| "Invalid cluster name: " + clusterName)); | ||
| } | ||
| try { | ||
| XdsResourceManager.parseYaml(body, Cluster.newBuilder()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question) Would it make sense to fork
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there's not so much gain for forking the class. If you think, this will be useful, let's consider adding yaml support to the |
||
| } catch (IOException e) { | ||
| return CompletableFuture.completedFuture( | ||
| XdsResourceManager.errorResponse(HttpStatus.BAD_REQUEST, | ||
| "Invalid request body: " + e.getMessage())); | ||
| } | ||
| final String updateSummary = isNullOrEmpty(summary) ? "Update cluster: " + clusterName : summary; | ||
| final String bodyToStore = XdsResourceManager.injectYamlField(body, "name", clusterName); | ||
| return xdsResourceManager.update(group, clusterName, updateSummary, currentAuthor(), bodyToStore); | ||
| } | ||
|
|
||
| private static Matcher checkClusterName(String clusterName) { | ||
| final Matcher matcher = CLUSTER_NAME_PATTERN.matcher(clusterName); | ||
| if (!matcher.matches()) { | ||
| throw Status.INVALID_ARGUMENT.withDescription("Invalid cluster name: " + clusterName + | ||
| " (expected: " + CLUSTER_NAME_PATTERN + ')') | ||
| .asRuntimeException(); | ||
| /** | ||
| * DELETE /xds/groups/{group}/clusters/{cluster_id} | ||
| * | ||
| * <p>Removes a cluster. | ||
| */ | ||
| @Delete("/xds/groups/{group}/clusters/{*cluster_id}") | ||
| @RequiresXdsGroupRole(RepositoryRole.WRITE) | ||
| public CompletableFuture<HttpResponse> deleteCluster( | ||
| @Param("group") String group, | ||
| @Param("cluster_id") String clusterId, | ||
| @Param("summary") @Nullable String summary) { | ||
| final String clusterName = "groups/" + group + "/clusters/" + clusterId; | ||
| if (!CLUSTER_NAME_PATTERN.matcher(clusterName).matches()) { | ||
| return CompletableFuture.completedFuture( | ||
| XdsResourceManager.errorResponse(HttpStatus.BAD_REQUEST, | ||
| "Invalid cluster name: " + clusterName)); | ||
| } | ||
| return matcher; | ||
| final String deleteSummary = isNullOrEmpty(summary) ? "Delete cluster: " + clusterName : summary; | ||
| return xdsResourceManager.delete(group, clusterName, deleteSummary, currentAuthor()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A summary seems like an arbitrary value set by users. Is it safe to set in the query parameters? I'm concerned about potential issues related to encoding or string length.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's going to be encoded. Also the length limit is more than 2000 chars so I think it should be fine.
https://medium.com/suyeonme/effective-strategies-for-handling-long-query-strings-b790e1fddd65
Let's revisit this if this becomes an issue. 😉