Skip to content

Commit e5ae13a

Browse files
authored
fix(miniflare): decouple KV plugin from Secrets Store plugin to avoid service name conflicts (#9106)
* refactor(miniflare): decouple kv plugin from the sercets store plugin * fixup! refactor(miniflare): decouple kv plugin from the sercets store plugin * add changeset * add regression test
1 parent cc7fae4 commit e5ae13a

File tree

4 files changed

+113
-81
lines changed

4 files changed

+113
-81
lines changed

.changeset/silver-bats-brush.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"miniflare": patch
3+
---
4+
5+
fix: decouple KV plugin from secrets store plugin
6+
7+
The KV plugin previously configured both KV namespace and secrets store bindings with the same service name but different persistence paths, causing conflicts when both were defined. This change copies the KV binding implementation into the secrets store plugin and customizes its service name to prevent collisions.

packages/miniflare/src/plugins/kv/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const KVSharedOptionsSchema = z.object({
5757

5858
const SERVICE_NAMESPACE_PREFIX = `${KV_PLUGIN_NAME}:ns`;
5959
const KV_STORAGE_SERVICE_NAME = `${KV_PLUGIN_NAME}:storage`;
60-
const KV_NAMESPACE_OBJECT_CLASS_NAME = "KVNamespaceObject";
60+
export const KV_NAMESPACE_OBJECT_CLASS_NAME = "KVNamespaceObject";
6161
const KV_NAMESPACE_OBJECT: Worker_Binding_DurableObjectNamespaceDesignator = {
6262
serviceName: SERVICE_NAMESPACE_PREFIX,
6363
className: KV_NAMESPACE_OBJECT_CLASS_NAME,
Lines changed: 101 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
import fs from "fs/promises";
2+
import SCRIPT_KV_NAMESPACE_OBJECT from "worker:kv/namespace";
13
import SCRIPT_SECRETS_STORE_SECRET from "worker:secrets-store/secret";
24
import { z } from "zod";
3-
import { ServiceDesignator, Worker_Binding } from "../../runtime";
4-
import { KV_PLUGIN, KVOptionsSchema } from "../kv";
5-
import { PersistenceSchema, Plugin, ProxyNodeBinding } from "../shared";
5+
import { Service, Worker_Binding } from "../../runtime";
6+
import { SharedBindings } from "../../workers";
7+
import { KV_NAMESPACE_OBJECT_CLASS_NAME } from "../kv";
8+
import {
9+
getMiniflareObjectBindings,
10+
getPersistPath,
11+
objectEntryWorker,
12+
PersistenceSchema,
13+
Plugin,
14+
ProxyNodeBinding,
15+
SERVICE_LOOPBACK,
16+
} from "../shared";
617

718
const SecretsStoreSecretsSchema = z.record(
819
z.object({
@@ -21,30 +32,6 @@ export const SecretsStoreSecretsSharedOptionsSchema = z.object({
2132

2233
export const SECRET_STORE_PLUGIN_NAME = "secrets-store";
2334

24-
function getkvNamespacesOptions(
25-
secretsStoreSecrets: z.input<typeof SecretsStoreSecretsSchema>
26-
): z.input<typeof KVOptionsSchema> {
27-
// Get unique store ids
28-
const storeIds = new Set(
29-
Object.values(secretsStoreSecrets).map((store) => store.store_id)
30-
);
31-
// Setup a KV Namespace per store id with store id as the binding name
32-
const storeIdKvNamespaceEntries = Array.from(storeIds).map((storeId) => [
33-
storeId,
34-
`${SECRET_STORE_PLUGIN_NAME}:${storeId}`,
35-
]);
36-
37-
return {
38-
kvNamespaces: Object.fromEntries(storeIdKvNamespaceEntries),
39-
};
40-
}
41-
42-
function isKvBinding(
43-
binding: Worker_Binding
44-
): binding is Worker_Binding & { kvNamespace: ServiceDesignator } {
45-
return "kvNamespace" in binding;
46-
}
47-
4835
export const SECRET_STORE_PLUGIN: Plugin<
4936
typeof SecretsStoreSecretsOptionsSchema,
5037
typeof SecretsStoreSecretsSharedOptionsSchema
@@ -80,65 +67,99 @@ export const SECRET_STORE_PLUGIN: Plugin<
8067
])
8168
);
8269
},
83-
async getServices({ options, sharedOptions, ...restOptions }) {
84-
if (!options.secretsStoreSecrets) {
70+
async getServices({ options, sharedOptions, tmpPath, unsafeStickyBlobs }) {
71+
const configs = options.secretsStoreSecrets
72+
? Object.values(options.secretsStoreSecrets)
73+
: [];
74+
75+
if (configs.length === 0) {
8576
return [];
8677
}
8778

88-
const kvServices = await KV_PLUGIN.getServices({
89-
options: getkvNamespacesOptions(options.secretsStoreSecrets),
90-
sharedOptions: {
91-
kvPersist: sharedOptions.secretsStorePersist,
92-
},
93-
...restOptions,
94-
});
95-
96-
const kvBindings = await KV_PLUGIN.getBindings(
97-
getkvNamespacesOptions(options.secretsStoreSecrets),
98-
restOptions.workerIndex
79+
const persistPath = getPersistPath(
80+
SECRET_STORE_PLUGIN_NAME,
81+
tmpPath,
82+
sharedOptions.secretsStorePersist
9983
);
10084

101-
if (!kvBindings || !kvBindings.every(isKvBinding)) {
102-
throw new Error(
103-
"Expected KV plugin to return bindings with kvNamespace defined"
104-
);
105-
}
106-
107-
if (!Array.isArray(kvServices)) {
108-
throw new Error("Expected KV plugin to return an array of services");
109-
}
85+
await fs.mkdir(persistPath, { recursive: true });
11086

111-
return [
112-
...kvServices,
113-
...Object.entries(options.secretsStoreSecrets).map<Worker_Binding>(
114-
([_, config]) => {
115-
return {
116-
name: `${SECRET_STORE_PLUGIN_NAME}:${config.store_id}:${config.secret_name}`,
117-
worker: {
118-
compatibilityDate: "2025-01-01",
119-
modules: [
120-
{
121-
name: "secret.worker.js",
122-
esModule: SCRIPT_SECRETS_STORE_SECRET(),
123-
},
124-
],
125-
bindings: [
126-
{
127-
name: "store",
128-
kvNamespace: kvBindings.find(
129-
// Look up the corresponding KV namespace for the store id
130-
(binding) => binding.name === config.store_id
131-
)?.kvNamespace,
132-
},
133-
{
134-
name: "secret_name",
135-
json: JSON.stringify(config.secret_name),
136-
},
137-
],
87+
const storageService = {
88+
name: `${SECRET_STORE_PLUGIN_NAME}:storage`,
89+
disk: { path: persistPath, writable: true },
90+
} satisfies Service;
91+
const objectService = {
92+
name: `${SECRET_STORE_PLUGIN_NAME}:ns`,
93+
worker: {
94+
compatibilityDate: "2023-07-24",
95+
compatibilityFlags: ["nodejs_compat", "experimental"],
96+
modules: [
97+
{
98+
name: "namespace.worker.js",
99+
esModule: SCRIPT_KV_NAMESPACE_OBJECT(),
100+
},
101+
],
102+
durableObjectNamespaces: [
103+
{
104+
className: KV_NAMESPACE_OBJECT_CLASS_NAME,
105+
uniqueKey: `miniflare-secrets-store-${KV_NAMESPACE_OBJECT_CLASS_NAME}`,
106+
},
107+
],
108+
// Store Durable Object SQL databases in persist path
109+
durableObjectStorage: { localDisk: storageService.name },
110+
// Bind blob disk directory service to object
111+
bindings: [
112+
{
113+
name: SharedBindings.MAYBE_SERVICE_BLOBS,
114+
service: { name: storageService.name },
115+
},
116+
{
117+
name: SharedBindings.MAYBE_SERVICE_LOOPBACK,
118+
service: { name: SERVICE_LOOPBACK },
119+
},
120+
...getMiniflareObjectBindings(unsafeStickyBlobs),
121+
],
122+
},
123+
} satisfies Service;
124+
const services = configs.flatMap<Service>((config) => {
125+
const kvNamespaceService = {
126+
name: `${SECRET_STORE_PLUGIN_NAME}:ns:${config.store_id}`,
127+
worker: objectEntryWorker(
128+
{
129+
serviceName: objectService.name,
130+
className: KV_NAMESPACE_OBJECT_CLASS_NAME,
131+
},
132+
config.store_id
133+
),
134+
} satisfies Service;
135+
const secretStoreSecretService = {
136+
name: `${SECRET_STORE_PLUGIN_NAME}:${config.store_id}:${config.secret_name}`,
137+
worker: {
138+
compatibilityDate: "2025-01-01",
139+
modules: [
140+
{
141+
name: "secret.worker.js",
142+
esModule: SCRIPT_SECRETS_STORE_SECRET(),
143+
},
144+
],
145+
bindings: [
146+
{
147+
name: "store",
148+
kvNamespace: {
149+
name: kvNamespaceService.name,
150+
},
151+
},
152+
{
153+
name: "secret_name",
154+
json: JSON.stringify(config.secret_name),
138155
},
139-
};
140-
}
141-
),
142-
];
156+
],
157+
},
158+
} satisfies Service;
159+
160+
return [kvNamespaceService, secretStoreSecretService];
161+
});
162+
163+
return [...services, storageService, objectService];
143164
},
144165
};

packages/wrangler/e2e/dev-with-resources.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,10 @@ describe.sequential.each(RUNTIMES)("Bindings: $flags", ({ runtime, flags }) => {
370370
name = "${workerName}"
371371
main = "src/index.ts"
372372
compatibility_date = "2025-01-01"
373+
# Regression test for https://github.com/cloudflare/workers-sdk/issues/9006
374+
kv_namespaces = [
375+
${isLocal ? `{ binding = "KV", id = "LOCAL_ONLY" }` : ""}
376+
]
373377
secrets_store_secrets = [
374378
{ binding = "SECRET", store_id = "${storeId}", secret_name = "${secret_name}" }
375379
]

0 commit comments

Comments
 (0)