Skip to content
Merged
13 changes: 13 additions & 0 deletions .changeset/hyperdrive-vpc-service-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"wrangler": minor
---

Add Workers VPC service support for Hyperdrive origins

Hyperdrive configs can now connect to databases through Workers VPC services using the `--service-id` option:

```bash
wrangler hyperdrive create my-config --service-id <vpc-service-uuid> --database mydb --user myuser --password mypassword
```

This enables Hyperdrive to connect to databases hosted in private networks that are accessible through Workers VPC TCP services.
19 changes: 19 additions & 0 deletions .changeset/vpc-cert-verification-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"wrangler": minor
---

Add `--cert-verification-mode` option to `wrangler vpc service create` and `wrangler vpc service update`

You can now configure the TLS certificate verification mode when creating or updating a VPC connectivity service. This controls how the connection to the origin server verifies TLS certificates.

Available modes:

- `verify_full` (default) -- verify certificate chain and hostname
- `verify_ca` -- verify certificate chain only, skip hostname check
- `disabled` -- do not verify the server certificate at all

```sh
wrangler vpc service create my-service --type tcp --tcp-port 5432 --ipv4 10.0.0.1 --tunnel-id <tunnel-uuid> --cert-verification-mode verify_ca
```

This applies to both TCP and HTTP VPC service types. When omitted, the default `verify_full` behavior is used.
13 changes: 13 additions & 0 deletions .changeset/vpc-tcp-service-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"wrangler": minor
---

Add TCP service type support for Workers VPC

You can now create TCP services in Workers VPC using the `--type tcp` option:

```bash
wrangler vpc service create my-db --type tcp --tcp-port 5432 --ipv4 10.0.0.1 --tunnel-id <tunnel-uuid>
```

This enables exposing TCP-based services like PostgreSQL, MySQL, and other database servers through Workers VPC.
214 changes: 207 additions & 7 deletions packages/wrangler/src/__tests__/hyperdrive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,125 @@ describe("hyperdrive commands", () => {
`);
});

it("should create a hyperdrive config with a VPC service ID", async ({
expect,
}) => {
const reqProm = mockHyperdriveCreate();
await runWrangler(
"hyperdrive create test123 --service-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --database=neondb --user=test --password=password"
);
await expect(reqProm).resolves.toMatchInlineSnapshot(`
{
"name": "test123",
"origin": {
"database": "neondb",
"password": "password",
"scheme": "postgresql",
"service_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"user": "test",
},
}
`);
expect(std.out).toMatchInlineSnapshot(`
"
⛅️ wrangler x.x.x
──────────────────
🚧 Creating 'test123'
✅ Created new Hyperdrive PostgreSQL config: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
To access your new Hyperdrive Config in your Worker, add the following snippet to your configuration file:
{
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
]
}"
`);
});

it("should create a hyperdrive config with a VPC service ID and mysql scheme", async ({
expect,
}) => {
const reqProm = mockHyperdriveCreate();
await runWrangler(
"hyperdrive create test123 --service-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --database=mydb --user=test --password=password --origin-scheme=mysql"
);
await expect(reqProm).resolves.toMatchInlineSnapshot(`
{
"name": "test123",
"origin": {
"database": "mydb",
"password": "password",
"scheme": "mysql",
"service_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"user": "test",
},
}
`);
expect(std.out).toMatchInlineSnapshot(`
"
⛅️ wrangler x.x.x
──────────────────
🚧 Creating 'test123'
✅ Created new Hyperdrive MySQL config: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
To access your new Hyperdrive Config in your Worker, add the following snippet to your configuration file:
{
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
]
}"
`);
});

it("should reject a create hyperdrive config with --service-id and --origin-host", async ({
expect,
}) => {
await expect(() =>
runWrangler(
"hyperdrive create test123 --service-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --origin-host=example.com --database=neondb --user=test --password=password"
)
).rejects.toThrow();
expect(std.err).toMatchInlineSnapshot(`
"X [ERROR] Arguments service-id and origin-host are mutually exclusive

"
`);
});

it("should reject a create hyperdrive config with --service-id and --connection-string", async ({
expect,
}) => {
await expect(() =>
runWrangler(
"hyperdrive create test123 --service-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --connection-string=postgresql://user:password@example.com:5432/neondb"
)
).rejects.toThrow();
expect(std.err).toMatchInlineSnapshot(`
"X [ERROR] Arguments service-id and connection-string are mutually exclusive

"
`);
});

it("should reject a create hyperdrive config with --service-id and --access-client-id", async ({
expect,
}) => {
await expect(() =>
runWrangler(
"hyperdrive create test123 --service-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --access-client-id=test.access --access-client-secret=secret --database=neondb --user=test --password=password"
)
).rejects.toThrow();
expect(std.err).toMatchInlineSnapshot(`
"X [ERROR] Arguments service-id and access-client-id are mutually exclusive

"
`);
});

it("should reject a create hyperdrive over access command if access client ID is set but not access client secret", async ({
expect,
}) => {
Expand Down Expand Up @@ -1530,6 +1649,79 @@ describe("hyperdrive commands", () => {
`);
});

it("should handle updating a hyperdrive config to use a VPC service ID", async ({
expect,
}) => {
const reqProm = mockHyperdriveUpdate();
await runWrangler(
"hyperdrive update xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --service-id=yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
);

await expect(reqProm).resolves.toMatchInlineSnapshot(`
{
"origin": {
"service_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
},
}
`);
expect(std.out).toMatchInlineSnapshot(`
"
⛅️ wrangler x.x.x
──────────────────
🚧 Updating 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
✅ Updated xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Hyperdrive config
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "test123",
"origin": {
"scheme": "postgresql",
"database": "neondb",
"user": "test",
"service_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
},
"origin_connection_limit": 25
}"
`);
});

it("should handle updating a hyperdrive config to use a VPC service ID with database credentials", async ({
expect,
}) => {
const reqProm = mockHyperdriveUpdate();
await runWrangler(
"hyperdrive update xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --service-id=yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy --database=newdb --origin-user=newuser --origin-password='passw0rd!'"
);

await expect(reqProm).resolves.toMatchInlineSnapshot(`
{
"origin": {
"database": "newdb",
"password": "passw0rd!",
"service_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"user": "newuser",
},
}
`);
expect(std.out).toMatchInlineSnapshot(`
"
⛅️ wrangler x.x.x
──────────────────
🚧 Updating 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
✅ Updated xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Hyperdrive config
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "test123",
"origin": {
"scheme": "postgresql",
"database": "newdb",
"user": "newuser",
"service_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
},
"origin_connection_limit": 25
}"
`);
});

it("should throw an exception when updating a hyperdrive config's origin but neither port nor access credentials are provided", async ({
expect,
}) => {
Expand Down Expand Up @@ -1928,7 +2120,12 @@ function mockHyperdriveUpdate(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} = reqBody.origin as any;
origin = { ...origin, ...reqOrigin };
if (reqOrigin.port) {
if (reqOrigin.service_id) {
delete origin.host;
delete origin.port;
delete origin.access_client_id;
delete origin.access_client_secret;
} else if (reqOrigin.port) {
delete origin.access_client_id;
delete origin.access_client_secret;
} else if (
Expand Down Expand Up @@ -1977,18 +2174,21 @@ function mockHyperdriveCreate(): Promise<CreateUpdateHyperdriveBody> {

resolve(reqBody);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const reqOrigin = reqBody.origin as any;
return HttpResponse.json(
createFetchResult(
{
id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
name: reqBody.name,
origin: {
host: reqBody.origin.host,
port: reqBody.origin.port,
database: reqBody.origin.database,
scheme: reqBody.origin.scheme,
user: reqBody.origin.user,
access_client_id: reqBody.origin.access_client_id,
host: reqOrigin.host,
port: reqOrigin.port,
database: reqOrigin.database,
scheme: reqOrigin.scheme,
user: reqOrigin.user,
access_client_id: reqOrigin.access_client_id,
service_id: reqOrigin.service_id,
},
caching: reqBody.caching,
mtls: reqBody.mtls,
Expand Down
Loading
Loading