Skip to content

Commit c8b7966

Browse files
authored
BP: Query gRPC service in Windmill (windmill-labs#443)
1 parent edb6dde commit c8b7966

File tree

2 files changed

+125
-0
lines changed

2 files changed

+125
-0
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
slug: query-grpc-service
3+
title: Querying gRPC service in Windmill
4+
authors: [guillaumebouv]
5+
tags: ['Windmill', 'gRPC', 'Developer tools']
6+
image: ./wmill_grpc.png
7+
---
8+
9+
To query a gRPC service, the client needs to know about its API definition (i.e. the `.proto` files). In some situations, the proto files are compiled in the desired language by the owner of the service and published as a package in the package repository. But when it's not the case, it can be cumbersome to query such a service.
10+
11+
In this post, we're going to see how you can easily workaround this limitation in Windmill using Bun and gRPC javascript `proto-loader` package.
12+
13+
### Example stack
14+
15+
This [docker-compose](https://github.com/windmill-labs/windmill/blob/main/examples/usecase/query-grpc-service/docker-compose.yml) spins up a stack with a single Windmill instance and a dummy gRPC service (the code can be found
16+
[here](https://github.com/gbouv/grpc-quickstart-service/tree/main)). It exposes the following API:
17+
18+
```proto
19+
syntax = "proto3";
20+
21+
option go_package = "github.com/gbouv/grpc-quickstart-service/protobuf";
22+
23+
package helloworld;
24+
25+
// The greeting service definition.
26+
service Greeter {
27+
// Sends a greeting
28+
rpc SayHello (HelloRequest) returns (HelloReply) {}
29+
}
30+
31+
// The request message containing the user's name.
32+
message HelloRequest {
33+
string name = 1;
34+
}
35+
36+
// The response message containing the greetings
37+
message HelloReply {
38+
string message = 1;
39+
}
40+
```
41+
42+
### gRPC query in Windmill
43+
44+
As said in the intro, most language requires to manually "compile" the `.proto` files to be able to use them (we explain below how this can also be done in Windmill).
45+
46+
Thankfully, Javascript has is able to dynamically build a client from the raw `.proto` files. Here we're going to use Bun which recently added support for the HTTP2 protocol used by gRPC.
47+
48+
First, we need to save the content of the `.proto` file. We're going to use a Windmill variable so that it can be used in multiple scripts. Here we save it to a variable named `service_proto`.
49+
50+
Once it's done, we create a Bun script in Windmill with the following content:
51+
52+
```js
53+
import * as wmill from "windmill-client"
54+
import * as grpc from "@grpc/grpc-js"
55+
import * as protoLoader from "@grpc/proto-loader"
56+
57+
const SERVICE_NAME = 'helloworld'
58+
59+
export async function main() {
60+
await writeProto()
61+
let service = await loadService()
62+
63+
let client = new service.Greeter("localhost:1353", grpc.credentials.createInsecure());
64+
return await query(client, "SayHello", { name: "Windmill!" })
65+
}
66+
67+
async function query(client, method, args): Promise<string> {
68+
return new Promise((resolve, reject) => {
69+
client[method](args, function(err, resp) {
70+
if (resp) {
71+
resolve(resp)
72+
} else {
73+
reject(err)
74+
}
75+
});
76+
})
77+
}
78+
79+
async function loadService() {
80+
var serviceDefinition = protoLoader.loadSync(
81+
"./service.proto",
82+
{
83+
keepCase: true,
84+
longs: String,
85+
enums: String,
86+
defaults: true,
87+
oneofs: true
88+
});
89+
return grpc.loadPackageDefinition(serviceDefinition)[SERVICE_NAME];
90+
}
91+
92+
async function writeProto() {
93+
const proto = await wmill.getVariable('u/admin/service_proto');
94+
await Bun.write("./service.proto", proto);
95+
}
96+
```
97+
98+
The logic is quite simple. The script starts by writing the content of the proto to a local file (unfortunately the `protoLoader.loadSync` function does not accept raw strings). Then uses `protoLoader.loadSync` to build a service client from the `.proto` content. And finally it queries the gRPC service using this client object. Note that the gRPC client is by default asynchronous and does not use the Promise mechanism. Here the `query` function simply wraps the call inside a Promise. And with all that, inside the main we can `query` the endpoint with its name and the request payload. The result is returned as the result of the script:
99+
```
100+
{
101+
"message": "Hello Windmill!"
102+
}
103+
```
104+
105+
This canonical script can be used in Flows to easily query the service and process the result in a following step.
106+
107+
### Statically defined gRPC services
108+
109+
If the `.proto` are not compiled by the service owner and you want to use another language than typescript (if if you're using javascript but don't want dynamic loading), you will have no other choice than to compile the `.proto` yourself. And then, to use the compiled service definition in Windmill, the easiest is to publish the files to a private package registry.
110+
111+
For Python for example, you can compile the `.proto` with:
112+
113+
```bash
114+
protoc --python_out=./ ./helloworld.proto
115+
```
116+
117+
This will generate a python file corresponding to your service definition in Python. You can then add this file to a python package of your choice, and publish it to a private Pypi repository (like [pypiserver](https://pypi.org/project/pypiserver/)). You can then (configure Windmill to use this repository)[https://www.windmill.dev/docs/advanced/imports#private-pypi-repository] and you will be able to pull the pre-compiled service definition from any python script in Windmill.
118+
119+
The same can be done for javascript. To compile the `.proto`, simply run:
120+
121+
```bash
122+
grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./ --grpc_out=grpc_js:./ helloworld.proto
123+
```
124+
125+
And then upload the content as a NPM package to a private NPM registry (like [verdaccio](https://verdaccio.org/)) and (configure Windmill to pull package from it)[https://www.windmill.dev/docs/advanced/imports#private-npm-registry].
Loading

0 commit comments

Comments
 (0)