Skip to content
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

gRPC apk-conf support #2353

Merged
merged 29 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bdbcaba
added grpc spec structs for ballerina
DDH13 Apr 19, 2024
aab6843
conf gets generated from proto file
DDH13 Apr 19, 2024
1c93b91
CRs get generated
DDH13 Apr 22, 2024
a616922
gRPC CRs get generated
DDH13 Apr 22, 2024
75a2b9f
fixed basepath and service path issue
DDH13 Apr 22, 2024
250f3ff
fixed TODOs
DDH13 Apr 22, 2024
41ae5cc
Add deploying api using apk-conf and proto file
DDH13 Apr 24, 2024
f424942
add cucumber tests
DDH13 Apr 24, 2024
7fb480c
add grpc backend deployment and service CRs
DDH13 Apr 24, 2024
5fa3111
add code needed for cucumber tests
DDH13 Apr 24, 2024
80be9cc
add more code for cucumber test
DDH13 Apr 25, 2024
781793c
cucumber test for grpc works
DDH13 Apr 25, 2024
55cf2ad
generalized interceptor
DDH13 Apr 26, 2024
2667f32
add cucumber test for oauth2 disabled
DDH13 Apr 26, 2024
3182a13
add cucumber test for scopes
DDH13 Apr 26, 2024
efc2d79
add cucumber test for default version
DDH13 Apr 26, 2024
9a5f436
add missing bracket to fix issue in commit bdc279f566d230cce3c195b5a…
DDH13 Apr 26, 2024
1d8ed4d
resolved comments
DDH13 Apr 26, 2024
3c10d00
changed grpc deployment and service CR
DDH13 Apr 29, 2024
064a678
Add handling for errors
DDH13 Apr 29, 2024
fe87a63
Slight change to test
DDH13 Apr 30, 2024
ddd0066
fixed api-definition-endpoint
DDH13 May 2, 2024
bbaa071
fixed api-definition-endpoint
DDH13 May 2, 2024
c2a0997
changed path match type to Exact
DDH13 May 2, 2024
ed46c2c
add cucumber test for api definition endpoint
DDH13 May 2, 2024
2282d77
Add name generation for apk-conf
DDH13 May 3, 2024
af1445e
prevent GRPC API from propagating to CP
DDH13 May 6, 2024
aaa9180
add cucumber test for mtls
DDH13 May 8, 2024
b433690
changed gRPC status code
DDH13 May 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions adapter/internal/operator/controllers/dp/api_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,9 @@ func (apiReconciler *APIReconciler) resolveAPIRefs(ctx context.Context, api dpv1

func isAPIPropagatable(apiState *synchronizer.APIState) bool {
validOrgs := []string{"carbon.super"}
if apiState.APIDefinition.Spec.APIType == "GRPC" {
return false
}
// System APIs should not be propagated to CP
if apiState.APIDefinition.Spec.SystemAPI {
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@
import org.wso2.apk.enforcer.constants.AdminConstants;
import org.wso2.apk.enforcer.constants.HttpConstants;
import org.wso2.apk.enforcer.models.ResponsePayload;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SwaggerServerHandler extends SimpleChannelInboundHandler<HttpObject> {

Expand All @@ -43,17 +46,34 @@ public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Excep

boolean isSwagger = false;

String [] params = request.uri().split("/");
final String basePath = "/" + params[1] + "/" + params[2];
final String vHost = params[3].split("\\?")[0];
final String queryParam = params[3].split("\\?")[1];
//check if it's GRPC request using the header
String header = request.headers().get("ApiType");
String[] params = request.uri().split("/");
final String basePath;
final String vHost;
final String queryParam;
final String version;
//if len params is 3, then it's a GRPC request else other
final String type = params.length == 3 ? "GRPC" : "REST";
if (type.equals("GRPC")) {
basePath = "/" + params[1];
vHost = params[2].split("\\?")[0];
queryParam = params[2].split("\\?")[1];
version = extractVersionFromGrpcBasePath(params[1]);
} else {
basePath = "/" + params[1] + "/" + params[2];
vHost = params[3].split("\\?")[0];
queryParam = params[3].split("\\?")[1];
version = params[2];
}


if (APIDefinitionConstants.SWAGGER_DEFINITION.equalsIgnoreCase(queryParam)) {
isSwagger = true;
}
if(isSwagger){
// load the corresponding swagger definition from the API name
byte[] apiDefinition = apiFactory.getAPIDefinition(basePath, params[2], vHost);
byte[] apiDefinition = apiFactory.getAPIDefinition(basePath, version, vHost);
if (apiDefinition == null || apiDefinition.length == 0) {
String error = AdminConstants.ErrorMessages.NOT_FOUND;
responsePayload = new ResponsePayload();
Expand Down Expand Up @@ -91,4 +111,15 @@ private void buildAndSendResponse(ChannelHandlerContext ctx, ResponsePayload res
httpResponse.headers().set(HTTP.CONTENT_LEN, httpResponse.content().readableBytes());
ctx.writeAndFlush(httpResponse);
}

private static String extractVersionFromGrpcBasePath(String input) {
// Pattern to match '.v' followed by digits and optional periods followed by more digits
Pattern pattern = Pattern.compile("\\.v\\d+(\\.\\d+)*");
Matcher matcher = pattern.matcher(input);

if (matcher.find()) {
return matcher.group().substring(1); // Returns the matched version
}
return "";
}
}
151 changes: 144 additions & 7 deletions runtime/config-deployer-service/ballerina/APIClient.bal
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public class APIClient {
encodedString = encodedString.substring(0, encodedString.length() - 1);
}
APKConf apkConf = {
name: api.getName(),
name: api.getType() == API_TYPE_GRPC ? self.getUniqueNameForGrpcApi(api.getName()) : api.getName(),
basePath: api.getBasePath().length() > 0 ? api.getBasePath() : encodedString,
version: api.getVersion(),
'type: api.getType() == "" ? API_TYPE_REST : api.getType().toUpperAscii()
Expand Down Expand Up @@ -280,6 +280,14 @@ public class APIClient {
return fullBasePath;
}

isolated function returnFullGRPCBasePath(string basePath, string 'version) returns string {
string fullBasePath = basePath;
if (!string:endsWith(basePath, 'version)) {
fullBasePath = string:'join(".", basePath, 'version);
}
return fullBasePath;
}

private isolated function constructURlFromK8sService(K8sService 'k8sService) returns string {
return <string>k8sService.protocol + "://" + string:'join(".", <string>k8sService.name, <string>k8sService.namespace, "svc.cluster.local") + ":" + k8sService.port.toString();
}
Expand Down Expand Up @@ -442,6 +450,19 @@ public class APIClient {
sandboxRoutes.push(gqlRoute.metadata.name);
}
}
} else if apkConf.'type == API_TYPE_GRPC{
k8sAPI.spec.basePath = self.returnFullGRPCBasePath(apkConf.basePath, apkConf.'version);
foreach model:GRPCRoute grpcRoute in apiArtifact.productionGrpcRoutes {
if grpcRoute.spec.rules.length() > 0 {
productionRoutes.push(grpcRoute.metadata.name);
}
}
foreach model:GRPCRoute grpcRoute in apiArtifact.sandboxGrpcRoutes {
if grpcRoute.spec.rules.length() > 0 {
sandboxRoutes.push(grpcRoute.metadata.name);
}
}

} else {
foreach model:HTTPRoute httpRoute in apiArtifact.productionHttpRoutes {
if httpRoute.spec.rules.length() > 0 {
Expand Down Expand Up @@ -532,6 +553,26 @@ public class APIClient {
apiArtifact.sandboxGqlRoutes.push(gqlRoute);
}
}
} else if apkConf.'type == API_TYPE_GRPC {
model:GRPCRoute grpcRoute = {
metadata:
{
name: uniqueId + "-" + endpointType + "-grpcroute-" + count.toString(),
labels: self.getLabels(apkConf, organization)
},
spec: {
parentRefs: self.generateAndRetrieveParentRefs(apkConf, uniqueId),
rules: check self.generateGRPCRouteRules(apiArtifact, apkConf, endpoint, endpointType, organization),
hostnames: self.getHostNames(apkConf, uniqueId, endpointType, organization)
}
};
if grpcRoute.spec.rules.length() > 0 {
if endpointType == PRODUCTION_TYPE {
apiArtifact.productionGrpcRoutes.push(grpcRoute);
} else {
apiArtifact.sandboxGrpcRoutes.push(grpcRoute);
}
}
} else {
model:HTTPRoute httpRoute = {
metadata:
Expand All @@ -553,10 +594,10 @@ public class APIClient {
}
}
}

return;
}


private isolated function generateAndRetrieveParentRefs(APKConf apkConf, string uniqueId) returns model:ParentReference[] {
string gatewayName = gatewayConfiguration.name;
string listenerName = gatewayConfiguration.listenerName;
Expand All @@ -571,7 +612,7 @@ public class APIClient {
APKOperations[]? operations = apkConf.operations;
if operations is APKOperations[] {
foreach APKOperations operation in operations {
model:HTTPRouteRule|model:GQLRouteRule|() routeRule = check self.generateRouteRule(apiArtifact, apkConf, endpoint, operation, endpointType, organization);
model:HTTPRouteRule|model:GQLRouteRule|model:GRPCRouteRule|() routeRule = check self.generateRouteRule(apiArtifact, apkConf, endpoint, operation, endpointType, organization);
if routeRule is model:HTTPRouteRule {
model:HTTPRouteFilter[]? filters = routeRule.filters;
if filters is () {
Expand Down Expand Up @@ -625,12 +666,72 @@ public class APIClient {
return httpRouteRules;
}

private isolated function generateGRPCRouteRules(model:APIArtifact apiArtifact, APKConf apkConf, model:Endpoint? endpoint, string endpointType, commons:Organization organization) returns model:GRPCRouteRule[]|commons:APKError|error {
model:GRPCRouteRule[] grpcRouteRules = [];
APKOperations[]? operations = apkConf.operations;
if operations is APKOperations[] {
foreach APKOperations operation in operations {
model:HTTPRouteRule|model:GQLRouteRule|model:GRPCRouteRule|() routeRule = check self.generateRouteRule(apiArtifact, apkConf, endpoint, operation, endpointType, organization);
if routeRule is model:GRPCRouteRule {
model:GRPCRouteFilter[]? filters = routeRule.filters;
if filters is () {
filters = [];
routeRule.filters = filters;
}
string disableAuthenticationRefName = self.retrieveDisableAuthenticationRefName(apkConf, endpointType, organization);
if !(operation.secured ?: true) {
if !apiArtifact.authenticationMap.hasKey(disableAuthenticationRefName) {
model:Authentication generateDisableAuthenticationCR = self.generateDisableAuthenticationCR(apiArtifact, apkConf, endpointType, organization);
apiArtifact.authenticationMap[disableAuthenticationRefName] = generateDisableAuthenticationCR;
}
model:GRPCRouteFilter disableAuthenticationFilter = {'type: "ExtensionRef", extensionRef: {group: "dp.wso2.com", kind: "Authentication", name: disableAuthenticationRefName}};
(<model:GRPCRouteFilter[]>filters).push(disableAuthenticationFilter);
}
string[]? scopes = operation.scopes;
if scopes is string[] {
int count = 1;
foreach string scope in scopes {
model:Scope scopeCr;
if apiArtifact.scopes.hasKey(scope) {
scopeCr = apiArtifact.scopes.get(scope);
} else {
scopeCr = self.generateScopeCR(apiArtifact, apkConf, organization, scope, count);
count = count + 1;
}
model:GRPCRouteFilter scopeFilter = {'type: "ExtensionRef", extensionRef: {group: "dp.wso2.com", kind: scopeCr.kind, name: scopeCr.metadata.name}};
(<model:GRPCRouteFilter[]>filters).push(scopeFilter);
}
}
if operation.rateLimit != () {
model:RateLimitPolicy? rateLimitPolicyCR = self.generateRateLimitPolicyCR(apkConf, operation.rateLimit, apiArtifact.uniqueId, operation, organization);
if rateLimitPolicyCR != () {
apiArtifact.rateLimitPolicies[rateLimitPolicyCR.metadata.name] = rateLimitPolicyCR;
model:GRPCRouteFilter rateLimitPolicyFilter = {'type: "ExtensionRef", extensionRef: {group: "dp.wso2.com", kind: "RateLimitPolicy", name: rateLimitPolicyCR.metadata.name}};
(<model:GRPCRouteFilter[]>filters).push(rateLimitPolicyFilter);
}
}
if operation.operationPolicies != () {
model:APIPolicy? apiPolicyCR = check self.generateAPIPolicyAndBackendCR(apiArtifact, apkConf, operation, operation.operationPolicies, organization, apiArtifact.uniqueId);
if apiPolicyCR != () {
apiArtifact.apiPolicies[apiPolicyCR.metadata.name] = apiPolicyCR;
model:GRPCRouteFilter apiPolicyFilter = {'type: "ExtensionRef", extensionRef: {group: "dp.wso2.com", kind: "APIPolicy", name: apiPolicyCR.metadata.name}};
(<model:GRPCRouteFilter[]>filters).push(apiPolicyFilter);
}
}
grpcRouteRules.push(routeRule);
}
}
}
return grpcRouteRules;
}


private isolated function generateGQLRouteRules(model:APIArtifact apiArtifact, APKConf apkConf, model:Endpoint? endpoint, string endpointType, commons:Organization organization) returns model:GQLRouteRule[]|commons:APKError|error {
model:GQLRouteRule[] gqlRouteRules = [];
APKOperations[]? operations = apkConf.operations;
if operations is APKOperations[] {
foreach APKOperations operation in operations {
model:HTTPRouteRule|model:GQLRouteRule|() routeRule = check self.generateRouteRule(apiArtifact, apkConf, endpoint, operation, endpointType, organization);
model:HTTPRouteRule|model:GQLRouteRule|model:GRPCRouteRule|() routeRule = check self.generateRouteRule(apiArtifact, apkConf, endpoint, operation, endpointType, organization);
if routeRule is model:GQLRouteRule {
model:GQLRouteFilter[]? filters = routeRule.filters;
if filters is () {
Expand Down Expand Up @@ -754,7 +855,7 @@ public class APIClient {
return authentication;
}

private isolated function generateRouteRule(model:APIArtifact apiArtifact, APKConf apkConf, model:Endpoint? endpoint, APKOperations operation, string endpointType, commons:Organization organization) returns model:HTTPRouteRule|model:GQLRouteRule|()|commons:APKError {
private isolated function generateRouteRule(model:APIArtifact apiArtifact, APKConf apkConf, model:Endpoint? endpoint, APKOperations operation, string endpointType, commons:Organization organization) returns model:HTTPRouteRule|model:GQLRouteRule|model:GRPCRouteRule|()|commons:APKError {
do {
EndpointConfigurations? endpointConfig = operation.endpointConfigurations;
model:Endpoint? endpointToUse = ();
Expand All @@ -778,7 +879,16 @@ public class APIClient {
} else {
return e909022("Provided Type currently not supported for GraphQL APIs.", error("Provided Type currently not supported for GraphQL APIs."));
}
} else {
} else if apkConf.'type == API_TYPE_GRPC {
model:GRPCRouteMatch[]|error routeMatches = self.retrieveGRPCMatches(apkConf, operation, organization);
if routeMatches is model:GRPCRouteMatch[] && routeMatches.length() > 0 {
model:GRPCRouteRule grpcRouteRule = {matches: routeMatches, backendRefs: self.retrieveGeneratedBackend(apkConf, endpointToUse, endpointType)};
return grpcRouteRule;
} else {
return e909022("Provided Type currently not supported for GRPC APIs.", error("Provided Type currently not supported for GRPC APIs."));
}
}
else {
model:HTTPRouteRule httpRouteRule = {matches: self.retrieveHTTPMatches(apkConf, operation, organization), backendRefs: self.retrieveGeneratedBackend(apkConf, endpointToUse, endpointType), filters: self.generateFilters(apiArtifact, apkConf, endpointToUse, operation, endpointType, organization)};
return httpRouteRule;
}
Expand Down Expand Up @@ -923,6 +1033,13 @@ public class APIClient {
return gqlRouteMatch;

}

private isolated function retrieveGRPCMatches(APKConf apkConf, APKOperations apiOperation, commons:Organization organization) returns model:GRPCRouteMatch[] {
model:GRPCRouteMatch[] grpcRouteMatch = [];
model:GRPCRouteMatch grpcRoute = self.retrieveGRPCRouteMatch(apiOperation);
grpcRouteMatch.push(grpcRoute);
return grpcRouteMatch;
}

private isolated function retrieveHttpRouteMatch(APKConf apkConf, APKOperations apiOperation, commons:Organization organization) returns model:HTTPRouteMatch {
return {method: <string>apiOperation.verb, path: {'type: "RegularExpression", value: self.retrievePathPrefix(apkConf.basePath, apkConf.'version, apiOperation.target ?: "/*", organization)}};
Expand All @@ -937,6 +1054,17 @@ public class APIClient {
}
}

private isolated function retrieveGRPCRouteMatch(APKOperations apiOperation) returns model:GRPCRouteMatch {
model:GRPCRouteMatch grpcRouteMatch = {
method: {
'type: "Exact",
'service: <string>apiOperation.target,
method: <string>apiOperation.verb
}
};
return grpcRouteMatch;
}

isolated function retrieveGeneratedSwaggerDefinition(APKConf apkConf, string? definition) returns string|json|commons:APKError|error {
runtimeModels:API api1 = runtimeModels:newAPI1();
api1.setName(apkConf.name);
Expand Down Expand Up @@ -973,6 +1101,10 @@ public class APIClient {
api1.setGraphQLSchema(definition);
return definition;
}
if apkConf.'type == API_TYPE_GRPC && definition is string {
api1.setProtoDefinition(definition);
return definition;
}
if definition is string && definition.toString().trim().length() > 0 {
retrievedDefinition = runtimeUtil:RuntimeAPICommonUtil_generateDefinition2(api1, definition);
} else {
Expand Down Expand Up @@ -1547,6 +1679,11 @@ public class APIClient {
return hashedValue.toBase16();
}

public isolated function getUniqueNameForGrpcApi(string concatanatedServices) returns string {
byte[] hashedValue = crypto:hashSha1(concatanatedServices.toBytes());
return hashedValue.toBase16();
}

public isolated function retrieveHttpRouteRefName(APKConf apkConf, string 'type, commons:Organization organization) returns string {
return uuid:createType1AsString();
}
Expand Down Expand Up @@ -1644,7 +1781,7 @@ public class APIClient {
} else if definitionFile.fileName.endsWith(".json") {
apiDefinition = definitionFileContent;
}
} else if apiType == API_TYPE_GRAPHQL {
} else if apiType == API_TYPE_GRAPHQL || apiType == API_TYPE_GRPC {
apiDefinition = definitionFileContent;
}
if apkConf is () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public class ConfigGeneratorClient {
private isolated function validateAndRetrieveDefinition(string 'type, string? url, byte[]? content, string? fileName) returns runtimeapi:APIDefinitionValidationResponse|runtimeapi:APIManagementException|error|commons:APKError {
runtimeapi:APIDefinitionValidationResponse|runtimeapi:APIManagementException|error validationResponse;
boolean typeAvailable = 'type.length() > 0;
string[] ALLOWED_API_DEFINITION_TYPES = [API_TYPE_REST, API_TYPE_GRAPHQL, "ASYNC"];
string[] ALLOWED_API_DEFINITION_TYPES = [API_TYPE_REST, API_TYPE_GRAPHQL, "ASYNC",API_TYPE_GRPC];
if !typeAvailable {
return e909005("type");
}
Expand Down Expand Up @@ -187,6 +187,14 @@ public class ConfigGeneratorClient {
string yamlString = check self.convertJsonToYaml(gqlRoute.toJsonString());
_ = check self.storeFile(yamlString, gqlRoute.metadata.name, zipDir);
}
foreach model:GRPCRoute grpcRoute in apiArtifact.productionGrpcRoutes {
string yamlString = check self.convertJsonToYaml(grpcRoute.toJsonString());
_ = check self.storeFile(yamlString, grpcRoute.metadata.name, zipDir);
}
foreach model:GRPCRoute grpcRoute in apiArtifact.sandboxGrpcRoutes {
string yamlString = check self.convertJsonToYaml(grpcRoute.toJsonString());
_ = check self.storeFile(yamlString, grpcRoute.metadata.name, zipDir);
}
foreach model:Backend backend in apiArtifact.backendServices {
string yamlString = check self.convertJsonToYaml(backend.toJsonString());
_ = check self.storeFile(yamlString, backend.metadata.name, zipDir);
Expand Down
Loading
Loading