-
Notifications
You must be signed in to change notification settings - Fork 14
feat: Add Secrets Store integration with Container helper methods #84
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
Open
Talador12
wants to merge
1
commit into
cloudflare:main
Choose a base branch
from
Talador12:feature/secrets-store
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+638
−12
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ import type { | |
WaitOptions, | ||
CancellationOptions, | ||
StartAndWaitForPortsOptions, | ||
SecretsStoreBinding, | ||
} from '../types'; | ||
import { generateId, parseTimeExpression } from './helpers'; | ||
import { DurableObject } from 'cloudflare:workers'; | ||
|
@@ -232,6 +233,7 @@ export class Container<Env = unknown> extends DurableObject<Env> { | |
envVars: ContainerStartOptions['env'] = {}; | ||
entrypoint: ContainerStartOptions['entrypoint']; | ||
enableInternet: ContainerStartOptions['enableInternet'] = true; | ||
secretsStoreBindings: SecretsStoreBinding[] = []; | ||
|
||
// ========================= | ||
// PUBLIC INTERFACE | ||
|
@@ -261,6 +263,12 @@ export class Container<Env = unknown> extends DurableObject<Env> { | |
if (options) { | ||
if (options.defaultPort !== undefined) this.defaultPort = options.defaultPort; | ||
if (options.sleepAfter !== undefined) this.sleepAfter = options.sleepAfter; | ||
if (options.secretsStoreBindings) this.secretsStoreBindings = options.secretsStoreBindings; | ||
} | ||
|
||
// Auto-detect Secrets Store bindings if not explicitly provided | ||
if (this.secretsStoreBindings.length === 0) { | ||
this.secretsStoreBindings = this.autoDetectSecretsStoreBindings(env); | ||
} | ||
|
||
// Create schedules table if it doesn't exist | ||
|
@@ -912,7 +920,11 @@ export class Container<Env = unknown> extends DurableObject<Env> { | |
enableInternet, | ||
}; | ||
|
||
if (envVars && Object.keys(envVars).length > 0) startConfig.env = envVars; | ||
// Merge Secrets Store environment variables with existing environment variables | ||
const secretsStoreEnvVars = this.setupSecretsStoreBindingEnvironment(); | ||
const mergedEnvVars = { ...secretsStoreEnvVars, ...envVars }; | ||
|
||
if (Object.keys(mergedEnvVars).length > 0) startConfig.env = mergedEnvVars; | ||
if (entrypoint) startConfig.entrypoint = entrypoint; | ||
|
||
this.renewActivityTimeout(); | ||
|
@@ -1289,6 +1301,155 @@ export class Container<Env = unknown> extends DurableObject<Env> { | |
return this.toSchedule(schedule); | ||
} | ||
|
||
// ========================== | ||
// SECRETS STORE BINDING HELPERS | ||
// ========================== | ||
|
||
/** | ||
* Generate environment variables for Secrets Store bindings | ||
* Creates SECRETS_{BINDING_NAME}_BINDING, SECRETS_{BINDING_NAME}_STORE_ID, and SECRETS_{BINDING_NAME}_SECRET_NAME environment variables | ||
* @private | ||
*/ | ||
private setupSecretsStoreBindingEnvironment(): Record<string, string> { | ||
const secretsEnvVars: Record<string, string> = {}; | ||
for (const binding of this.secretsStoreBindings) { | ||
const envPrefix = binding.binding.toUpperCase().replace(/[^A-Z0-9]/g, '_'); | ||
secretsEnvVars[`SECRETS_${envPrefix}_BINDING`] = binding.binding; | ||
secretsEnvVars[`SECRETS_${envPrefix}_STORE_ID`] = binding.storeId; | ||
secretsEnvVars[`SECRETS_${envPrefix}_SECRET_NAME`] = binding.secretName; | ||
} | ||
return secretsEnvVars; | ||
} | ||
|
||
/** | ||
* Get detailed information about configured Secrets Store bindings | ||
* @public | ||
*/ | ||
public getSecretsStoreBindingInfo(): Record<string, { binding: string; storeId: string; secretName: string; envVars: Record<string, string> }> { | ||
const bindingInfo: Record<string, any> = {}; | ||
for (const binding of this.secretsStoreBindings) { | ||
const envPrefix = binding.binding.toUpperCase().replace(/[^A-Z0-9]/g, '_'); | ||
const envVars: Record<string, string> = { | ||
[`SECRETS_${envPrefix}_BINDING`]: binding.binding, | ||
[`SECRETS_${envPrefix}_STORE_ID`]: binding.storeId, | ||
[`SECRETS_${envPrefix}_SECRET_NAME`]: binding.secretName | ||
}; | ||
|
||
bindingInfo[binding.binding] = { | ||
binding: binding.binding, | ||
storeId: binding.storeId, | ||
secretName: binding.secretName, | ||
envVars | ||
}; | ||
} | ||
return bindingInfo; | ||
} | ||
|
||
/** | ||
* Validate Secrets Store binding environment variables are properly set | ||
* @public | ||
*/ | ||
public validateSecretsStoreBindingEnvironment(): { valid: boolean; bindings: Record<string, any>; errors: string[] } { | ||
const errors: string[] = []; | ||
const bindings: Record<string, any> = {}; | ||
|
||
// Get all Secrets Store environment variables from process.env | ||
const secretsVars: Record<string, string> = {}; | ||
for (const [key, value] of Object.entries(process.env)) { | ||
if (key.startsWith('SECRETS_') && value) { | ||
secretsVars[key] = value; | ||
} | ||
} | ||
|
||
// Group Secrets Store variables by binding name | ||
const groupedBindings: Record<string, Record<string, string>> = {}; | ||
for (const [key, value] of Object.entries(secretsVars)) { | ||
const match = key.match(/^SECRETS_([A-Z0-9_]+)_(BINDING|STORE_ID|SECRET_NAME)$/); | ||
if (match) { | ||
const bindingName = match[1]; | ||
const varType = match[2]; | ||
if (!groupedBindings[bindingName]) { | ||
groupedBindings[bindingName] = {}; | ||
} | ||
groupedBindings[bindingName][varType] = value; | ||
} | ||
} | ||
|
||
// Check each configured binding | ||
for (const binding of this.secretsStoreBindings) { | ||
const envPrefix = binding.binding.toUpperCase().replace(/[^A-Z0-9]/g, '_'); | ||
const foundBinding = groupedBindings[envPrefix]; | ||
|
||
if (!foundBinding) { | ||
errors.push(`Secrets Store binding '${binding.binding}' not found in environment variables`); | ||
continue; | ||
} | ||
|
||
if (!foundBinding.BINDING) { | ||
errors.push(`SECRETS_${envPrefix}_BINDING environment variable missing`); | ||
} | ||
|
||
if (!foundBinding.STORE_ID) { | ||
errors.push(`SECRETS_${envPrefix}_STORE_ID environment variable missing`); | ||
} | ||
|
||
if (!foundBinding.SECRET_NAME) { | ||
errors.push(`SECRETS_${envPrefix}_SECRET_NAME environment variable missing`); | ||
} | ||
|
||
bindings[binding.binding] = { | ||
configured: binding, | ||
environment: foundBinding, | ||
valid: foundBinding.BINDING && foundBinding.STORE_ID && foundBinding.SECRET_NAME | ||
}; | ||
} | ||
|
||
return { | ||
valid: errors.length === 0, | ||
bindings, | ||
errors | ||
}; | ||
} | ||
|
||
/** | ||
* Get a summary of Secrets Store bindings for logging and debugging | ||
* @public | ||
*/ | ||
public getSecretsStoreBindingSummary(): { configured: number; bindings: Array<{ name: string; storeId: string; secretName: string }> } { | ||
return { | ||
configured: this.secretsStoreBindings.length, | ||
bindings: this.secretsStoreBindings.map(binding => ({ | ||
name: binding.binding, | ||
storeId: binding.storeId, | ||
secretName: binding.secretName | ||
})) | ||
}; | ||
} | ||
|
||
/** | ||
* Auto-detect Secrets Store bindings from the environment | ||
* @private | ||
*/ | ||
private autoDetectSecretsStoreBindings(env: any): SecretsStoreBinding[] { | ||
const secretsStoreBindings: SecretsStoreBinding[] = []; | ||
|
||
// Look for Secrets Store binding properties in the environment | ||
for (const [key, value] of Object.entries(env)) { | ||
// Check if this property looks like a Secrets Store binding | ||
if (value && typeof value === 'object' && 'get' in value && typeof value.get === 'function') { | ||
// This looks like a Secrets Store binding - we need store_id and secret_name | ||
// For auto-detection, we'll use sensible defaults based on the binding name | ||
secretsStoreBindings.push({ | ||
binding: key, | ||
storeId: `auto-detected-store-${key.toLowerCase()}`, | ||
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. that's not ever going to correspond to an actual secret store id? |
||
secretName: key.toLowerCase().replace(/_/g, '-') | ||
}); | ||
} | ||
} | ||
|
||
return secretsStoreBindings; | ||
} | ||
|
||
private isActivityExpired(): boolean { | ||
return this.sleepAfterMs <= Date.now(); | ||
} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// Mock for cloudflare:workers module | ||
class DurableObject { | ||
constructor(ctx, env) { | ||
this.ctx = ctx; | ||
this.env = env; | ||
} | ||
|
||
async fetch(request) { | ||
return new Response('Mock DurableObject response'); | ||
} | ||
} | ||
|
||
module.exports = { DurableObject }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I don't know if you can detect a binding is a secret store from the runtime, but its definitely not this simple.
This will return loads of other things, KV namespaces, service bindings etc.
probably the closes thing is to check
value.constructor.name
- that'll work for KV and R2, but not secret store though.