From 7d27200af1799c9e35d0a9f8ae63ce6f9ba306b0 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Mon, 4 Aug 2025 13:58:44 -0400 Subject: [PATCH 01/23] Adds interfaces for configuration providers and resolvers --- packages/framework-types/src/config.ts | 60 +++++ .../src/configuration-resolver.ts | 82 +++++++ packages/framework-types/src/index.ts | 1 + .../test/configuration-resolver.test.ts | 218 ++++++++++++++++++ 4 files changed, 361 insertions(+) create mode 100644 packages/framework-types/src/configuration-resolver.ts create mode 100644 packages/framework-types/test/configuration-resolver.test.ts diff --git a/packages/framework-types/src/config.ts b/packages/framework-types/src/config.ts index 70e555acc..fc2a8e8e4 100644 --- a/packages/framework-types/src/config.ts +++ b/packages/framework-types/src/config.ts @@ -27,6 +27,66 @@ import { TraceConfiguration } from './instrumentation/trace-types' import { Context } from 'effect' import { AzureConfiguration, DEFAULT_CHUNK_SIZE } from './provider/azure-configuration' +/** + * Configuration provider interface for external configuration sources + */ +export interface ConfigurationProvider { + /** + * Retrieve a configuration value by key + * @param key The configuration key to retrieve + * @returns Promise resolving to the configuration value or undefined if not found + */ + getValue(key: string): Promise + + /** + * Check if the configuration provider is available and properly initialized + * @returns Promise resolving to a true if available, false otherwise + */ + isAvailable(): Promise + + /** + * Priority of this configuration provider (higher number = higher priority + */ + readonly priority: number + + /** + * Name identifier for this configuration provider + */ + readonly name: string +} + +/** + * Configuration resolution result with source tracking + */ +export interface ConfiguratonResolution { + value: string | undefined + source: string + key: string +} + +/** + * Configuration resolver that manages multiple providers with fallback + */ +export interface ConfigurationResolver { + /** + * Resolve a configuration value from all available providers + * @param key The configuration key to resolve + * @returns Promise resolving to the configuration resolution result + */ + resolve(key: string): Promise + + /** + * Add a configuration provider + * @param provider The configuration provider to add + */ + addProvider(provider: ConfigurationProvider): void + + /** + * Get all registered providers sorted by priority + */ + getProviders(): ConfigurationProvider[] +} + /** * Class used by external packages that needs to get a representation of * the booster config. Used mainly for vendor-specific deployment packages diff --git a/packages/framework-types/src/configuration-resolver.ts b/packages/framework-types/src/configuration-resolver.ts new file mode 100644 index 000000000..889f88b12 --- /dev/null +++ b/packages/framework-types/src/configuration-resolver.ts @@ -0,0 +1,82 @@ +import { ConfigurationProvider, ConfigurationResolver, ConfiguratonResolution } from './config' + +export class DefaultConfigurationResolver implements ConfigurationResolver { + private providers: ConfigurationProvider[] = [] + + constructor(providers: ConfigurationProvider[] = []) { + this.providers = [...providers].sort((a, b) => b.priority - a.priority) + } + + addProvider(provider: ConfigurationProvider): void { + // Remove any existing provider with the same name + const existingIndex = this.providers.findIndex((p) => p.name === provider.name) + if (existingIndex >= 0) { + this.providers.splice(existingIndex, 1) + } + + // Add the new provider and sort by priority (highest first) + this.providers.push(provider) + this.providers.sort((a, b) => b.priority - a.priority) + } + + getProviders(): ConfigurationProvider[] { + return [...this.providers] + } + + async resolve(key: string): Promise { + // Try each provider in priority order + for (const provider of this.providers) { + try { + // Check if provider is available before attempting to get value + if (await provider.isAvailable()) { + const value = await provider.getValue(key) + if (value !== undefined) { + return { value, source: provider.name, key } + } + } + } catch (error) { + // Log error but continue to next provider + console.warn(`Configuration provider '${provider.name}' failed to resolve key '${key}':`, error) + } + } + + // No provider could resolve the value + return { value: undefined, source: 'none', key } + } +} + +/** + * Environment variables configuration provider + * This is the fallback provider that reads from process.env + */ +export class EnvironmentVariablesProvider implements ConfigurationProvider { + readonly name = 'environment-variables' + readonly priority = 0 // Lowest priority - fallback provider + + async getValue(key: string): Promise { + return process.env[key] + } + + async isAvailable(): Promise { + return true // Environment variables are always available + } +} + +/** + * Booster config.env provider + * Reads from the Booster configuration env object + */ +export class BoosterConfigEnvProvider implements ConfigurationProvider { + readonly name = 'booster-config-env' + readonly priority = 10 // Medium priority + + constructor(private readonly envConfig: Record) {} + + async getValue(key: string): Promise { + return this.envConfig[key] + } + + async isAvailable(): Promise { + return true // Booster config env is always available + } +} diff --git a/packages/framework-types/src/index.ts b/packages/framework-types/src/index.ts index 18948278b..5a9003069 100644 --- a/packages/framework-types/src/index.ts +++ b/packages/framework-types/src/index.ts @@ -1,6 +1,7 @@ export * from './provider' export * from './envelope' export * from './config' +export * from './configuration-resolver' export * from './concepts' export * from './typelevel' export * from './logger' diff --git a/packages/framework-types/test/configuration-resolver.test.ts b/packages/framework-types/test/configuration-resolver.test.ts new file mode 100644 index 000000000..c83fb4f9c --- /dev/null +++ b/packages/framework-types/test/configuration-resolver.test.ts @@ -0,0 +1,218 @@ +import { + BoosterConfigEnvProvider, + ConfigurationProvider, + DefaultConfigurationResolver, + EnvironmentVariablesProvider, +} from '../src' +import { expect } from './expect' + +// Mock configuration provider for testing +class MockConfigurationProvider implements ConfigurationProvider { + constructor( + public readonly name: string, + public readonly priority: number, + private readonly values: Record = {}, + private readonly available: boolean = true + ) {} + + async getValue(key: string): Promise { + return this.values[key] + } + + async isAvailable(): Promise { + return this.available + } +} + +describe('DefaultConfigurationResolver', () => { + let resolver: DefaultConfigurationResolver + + beforeEach(() => { + resolver = new DefaultConfigurationResolver() + }) + + describe('constructor', () => { + it('should create an empty resolver', () => { + expect(resolver.getProviders()).to.have.length(0) + }) + + it('should create resolver with initial providers', () => { + const provider1 = new MockConfigurationProvider('test1', 10) + const provider2 = new MockConfigurationProvider('test2', 20) + + const resolverWithProviders = new DefaultConfigurationResolver([provider1, provider2]) + + const providers = resolverWithProviders.getProviders() + expect(providers).to.have.length(2) + expect(providers[0].name).to.equal('test2') // Higher priority first + expect(providers[1].name).to.equal('test1') + }) + }) + + describe('addProvider', () => { + it('should add providers and sort by priority', () => { + const provider1 = new MockConfigurationProvider('test1', 10) + const provider2 = new MockConfigurationProvider('test2', 20) + const provider3 = new MockConfigurationProvider('test3', 15) + + resolver.addProvider(provider1) + resolver.addProvider(provider2) + resolver.addProvider(provider3) + + const providers = resolver.getProviders() + expect(providers).to.have.length(3) + expect(providers[0].name).to.equal('test2') // Priority 20 + expect(providers[1].name).to.equal('test3') // Priority 15 + expect(providers[2].name).to.equal('test1') // Priority 10 + }) + + it('should replace provider with same name', () => { + const provider1 = new MockConfigurationProvider('test', 10, { key: 'low-priority' }) + const provider2 = new MockConfigurationProvider('test', 20, { key: 'high-priority' }) + + resolver.addProvider(provider1) + resolver.addProvider(provider2) + + const providers = resolver.getProviders() + expect(providers).to.have.length(1) + expect(providers[0].priority).to.equal(20) + }) + }) + + describe('resolve', () => { + it('should resolve from highest priority provider', async () => { + const provider1 = new MockConfigurationProvider('test1', 10, { key1: 'low-priority' }) + const provider2 = new MockConfigurationProvider('test2', 20, { key1: 'high-priority' }) + + resolver.addProvider(provider1) + resolver.addProvider(provider2) + + const result = await resolver.resolve('key1') + expect(result.value).to.equal('high-priority') + expect(result.source).to.equal('test2') + expect(result.key).to.equal('key1') + }) + + it('should fallback to lower priority providers', async () => { + const provider1 = new MockConfigurationProvider('test1', 10, { key1: 'fallback-value' }) + const provider2 = new MockConfigurationProvider('test2', 20, {}) // No key1 + + resolver.addProvider(provider1) + resolver.addProvider(provider2) + + const result = await resolver.resolve('key1') + expect(result.value).to.equal('fallback-value') + expect(result.source).to.equal('test1') + }) + + it('should return undefined when no provider has the value', async () => { + const provider1 = new MockConfigurationProvider('test1', 10, {}) + const provider2 = new MockConfigurationProvider('test2', 20, {}) + + resolver.addProvider(provider1) + resolver.addProvider(provider2) + + const result = await resolver.resolve('nonexistent-key') + expect(result.value).to.be.undefined + expect(result.source).to.equal('none') + expect(result.key).to.equal('nonexistent-key') + }) + + it('should skip unavailable providers', async () => { + const provider1 = new MockConfigurationProvider('test1', 10, { key1: 'unavailable-value' }, false) + const provider2 = new MockConfigurationProvider('test2', 20, { key1: 'available-value' }, true) + + resolver.addProvider(provider1) + resolver.addProvider(provider2) + + const result = await resolver.resolve('key1') + expect(result.value).to.equal('available-value') + expect(result.source).to.equal('test2') + }) + + it('should handle provider errors gracefully', async () => { + const errorProvider = new MockConfigurationProvider('error-provider', 20) + // Mock getValue to throw an error + errorProvider.getValue = async () => { + throw new Error('Provider error') + } + + const fallbackProvider = new MockConfigurationProvider('fallback', 10, { key1: 'fallback-value' }) + + resolver.addProvider(errorProvider) + resolver.addProvider(fallbackProvider) + + const result = await resolver.resolve('key1') + expect(result.value).to.equal('fallback-value') + expect(result.source).to.equal('fallback') + }) + }) +}) + +describe('EnvironmentVariablesProvider', () => { + let provider: EnvironmentVariablesProvider + let originalEnv: typeof process.env + + beforeEach(() => { + provider = new EnvironmentVariablesProvider() + originalEnv = { ...process.env } + }) + + afterEach(() => { + process.env = originalEnv + }) + + it('should have correct name and priority', () => { + expect(provider.name).to.equal('environment-variables') + expect(provider.priority).to.equal(0) + }) + + it('should always be available', async () => { + const available = await provider.isAvailable() + expect(available).to.be.true + }) + + it('should get values from process.env', async () => { + process.env['TEST_VAR'] = 'test-value' + + const value = await provider.getValue('TEST_VAR') + expect(value).to.equal('test-value') + }) + + it('should return undefined for missing variables', async () => { + const value = await provider.getValue('NONEXISTENT_VAR') + expect(value).to.be.undefined + }) +}) + +describe('BoosterConfigEnvProvider', () => { + let provider: BoosterConfigEnvProvider + + beforeEach(() => { + const envConfig = { + VAR1: 'value1', + VAR2: 'value2', + } + provider = new BoosterConfigEnvProvider(envConfig) + }) + + it('should have correct name and priority', () => { + expect(provider.name).to.equal('booster-config-env') + expect(provider.priority).to.equal(10) + }) + + it('should always be available', async () => { + const available = await provider.isAvailable() + expect(available).to.be.true + }) + + it('should get values from config.env', async () => { + const value = await provider.getValue('VAR1') + expect(value).to.equal('value1') + }) + + it('should return undefined for missing variables', async () => { + const value = await provider.getValue('NONEXISTENT_VAR') + expect(value).to.be.undefined + }) +}) From d36c154a783e86e63eb08cb3514d7408f9de9097 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Mon, 4 Aug 2025 14:59:27 -0400 Subject: [PATCH 02/23] Adds configuration service to core package --- packages/framework-core/src/index.ts | 1 + .../src/services/configuration-service.ts | 85 ++++++ .../services/configuration-service.test.ts | 259 ++++++++++++++++++ packages/framework-types/src/config.ts | 40 +++ 4 files changed, 385 insertions(+) create mode 100644 packages/framework-core/src/services/configuration-service.ts create mode 100644 packages/framework-core/test/services/configuration-service.test.ts diff --git a/packages/framework-core/src/index.ts b/packages/framework-core/src/index.ts index ca41b8b13..5d668f300 100644 --- a/packages/framework-core/src/index.ts +++ b/packages/framework-core/src/index.ts @@ -17,6 +17,7 @@ export { BoosterDataMigrationFinished } from './core-concepts/data-migration/eve export { BoosterDataMigrationEntity } from './core-concepts/data-migration/entities/booster-data-migration-entity' export { BoosterTouchEntityHandler } from './booster-touch-entity-handler' export * from './services/token-verifiers' +export * from './services/configuration-service' export * from './instrumentation/index' export * from './decorators/health-sensor' export * as Injectable from './injectable' diff --git a/packages/framework-core/src/services/configuration-service.ts b/packages/framework-core/src/services/configuration-service.ts new file mode 100644 index 000000000..f4a512512 --- /dev/null +++ b/packages/framework-core/src/services/configuration-service.ts @@ -0,0 +1,85 @@ +import { + BoosterConfig, + BoosterConfigEnvProvider, + ConfigurationResolver, + DefaultConfigurationResolver, + EnvironmentVariablesProvider, +} from '@boostercloud/framework-types' + +export class ConfigurationService { + private static instance: ConfigurationService | undefined + private resolver: ConfigurationResolver + + private constructor(config: BoosterConfig) { + this.resolver = new DefaultConfigurationResolver() + + // Add default providers (these are always available) + this.resolver.addProvider(new EnvironmentVariablesProvider()) + this.resolver.addProvider(new BoosterConfigEnvProvider(config.env)) + + // Add any registered configuration providers from the config + for (const provider of config.configurationProviders) { + this.resolver.addProvider(provider) + } + } + + /** + * Get the singleton instance of the ConfigurationService + */ + public static getInstance(config: BoosterConfig): ConfigurationService { + if (!this.instance) { + this.instance = new ConfigurationService(config) + } + return this.instance + } + + /** + * Reset the singleton instance (useful for testing) + */ + public static reset(): void { + this.instance = undefined + } + + /** + * Resolve a configuration value from all available providers + * @param key The configuration key to resolve + * @returns Promise resolving to the configuration value or undefined if not found + */ + public async getValue(key: string): Promise { + const resolution = await this.resolver.resolve(key) + return resolution.value + } + + /** + * Resolve a configuration value with source tracking + * @param key The configuration key to resolve + * @returns Promise resolving to the full configuration resolution result + */ + public async resolve(key: string) { + return this.resolver.resolve(key) + } + + /** + * Get all registered providers + */ + public getProviders() { + return this.resolver.getProviders() + } +} + +/** + * Utility function to resolve a configuration value using the Booster configuration + * This is the main API for configuration resolution within the framework + */ +export async function resolveConfigurationValue(config: BoosterConfig, key: string): Promise { + const configService = ConfigurationService.getInstance(config) + return configService.getValue(key) +} + +/** + * Utility function to resolve a configuration value with source tracking + */ +export async function resolveConfigurationWithSource(config: BoosterConfig, key: string) { + const configService = ConfigurationService.getInstance(config) + return configService.resolve(key) +} diff --git a/packages/framework-core/test/services/configuration-service.test.ts b/packages/framework-core/test/services/configuration-service.test.ts new file mode 100644 index 000000000..50d0587a4 --- /dev/null +++ b/packages/framework-core/test/services/configuration-service.test.ts @@ -0,0 +1,259 @@ +// Mock configuration provider for testing +import { BoosterConfig, ConfigurationProvider } from '@boostercloud/framework-types' +import { ConfigurationService, resolveConfigurationValue, resolveConfigurationWithSource } from '../../src' +import { restore } from 'sinon' +import { expect } from '../expect' + +class MockConfigurationProvider implements ConfigurationProvider { + constructor( + public readonly name: string, + public readonly priority: number, + private readonly values: Record = {}, + private readonly available: boolean = true + ) {} + + async getValue(key: string): Promise { + return this.values[key] + } + + async isAvailable(): Promise { + return this.available + } +} + +describe('ConfigurationService', () => { + let mockConfig: BoosterConfig + + beforeEach(() => { + ConfigurationService.reset() + mockConfig = new BoosterConfig('test') + // Override the readonly env property for testing + Object.assign(mockConfig, { + env: { + CONFIG_VAR: 'config-value', + SHARED_VAR: 'config-shared-value', + }, + }) + }) + + afterEach(() => { + restore() + ConfigurationService.reset() + }) + + describe('getInstance', () => { + it('should create singleton instance', () => { + const instance1 = ConfigurationService.getInstance(mockConfig) + const instance2 = ConfigurationService.getInstance(mockConfig) + + expect(instance1).to.equal(instance2) + }) + + it('should initialize with default providers', () => { + const instance = ConfigurationService.getInstance(mockConfig) + const providers = instance.getProviders() + + expect(providers).to.have.length(2) + expect(providers.some((p) => p.name === 'environment-variables')).to.be.true + expect(providers.some((p) => p.name === 'booster-config-env')).to.be.true + }) + + it('should include registered configuration providers', () => { + const customProvider = new MockConfigurationProvider('custom', 25) + mockConfig.addConfigurationProvider(customProvider) + + const instance = ConfigurationService.getInstance(mockConfig) + const providers = instance.getProviders() + + expect(providers).to.have.length(3) + expect(providers.some((p) => p.name === 'custom')).to.be.true + expect(providers[0].name).to.equal('custom') // Highest priority first + }) + }) + + describe('getValue', () => { + let originalEnv: typeof process.env + + beforeEach(() => { + originalEnv = { ...process.env } + }) + + afterEach(() => { + process.env = originalEnv + }) + + it('should resolve from highest priority provider', async () => { + // Set up environment variable + process.env['TEST_VAR'] = 'env-value' + + // Add higher priority provider + const highPriorityProvider = new MockConfigurationProvider('high-priority', 25, { + TEST_VAR: 'high-priority-value', + }) + mockConfig.addConfigurationProvider(highPriorityProvider) + + const instance = ConfigurationService.getInstance(mockConfig) + const value = await instance.getValue('TEST_VAR') + + expect(value).to.equal('high-priority-value') + }) + + it('should fallback to config.env', async () => { + const instance = ConfigurationService.getInstance(mockConfig) + const value = await instance.getValue('CONFIG_VAR') + + expect(value).to.equal('config-value') + }) + + it('should fallback to environment variables', async () => { + process.env['ENV_ONLY_VAR'] = 'env-only-value' + + const instance = ConfigurationService.getInstance(mockConfig) + const value = await instance.getValue('ENV_ONLY_VAR') + + expect(value).to.equal('env-only-value') + }) + + it('should return undefined for nonexistent keys', async () => { + const instance = ConfigurationService.getInstance(mockConfig) + const value = await instance.getValue('NONEXISTENT_KEY') + + expect(value).to.be.undefined + }) + }) + + describe('resolve', () => { + it('should return resolution with source tracking', async () => { + const customProvider = new MockConfigurationProvider('custom', 20, { + TEST_KEY: 'custom-value', + }) + mockConfig.addConfigurationProvider(customProvider) + + const instance = ConfigurationService.getInstance(mockConfig) + const resolution = await instance.resolve('TEST_KEY') + + expect(resolution.value).to.equal('custom-value') + expect(resolution.source).to.equal('custom') + expect(resolution.key).to.equal('TEST_KEY') + }) + + it('should track source as none when no provider resolves', async () => { + const instance = ConfigurationService.getInstance(mockConfig) + const resolution = await instance.resolve('NONEXISTENT_KEY') + + expect(resolution.value).to.be.undefined + expect(resolution.source).to.equal('none') + expect(resolution.key).to.equal('NONEXISTENT_KEY') + }) + }) +}) + +describe('utility functions', () => { + let mockConfig: BoosterConfig + + beforeEach(() => { + ConfigurationService.reset() + mockConfig = new BoosterConfig('test') + // Override the readonly env property for testing + Object.assign(mockConfig, { + env: { + UTIL_TEST_VAR: 'util-config-value', + }, + }) + }) + + afterEach(() => { + ConfigurationService.reset() + }) + + describe('resolveConfigurationValue', () => { + it('should resolve configuration value', async () => { + const value = await resolveConfigurationValue(mockConfig, 'UTIL_TEST_VAR') + expect(value).to.equal('util-config-value') + }) + + it('should return undefined for missing values', async () => { + const value = await resolveConfigurationValue(mockConfig, 'MISSING_VAR') + expect(value).to.be.undefined + }) + }) + + describe('resolveConfigurationWithSource', () => { + it('should resolve with source information', async () => { + const resolution = await resolveConfigurationWithSource(mockConfig, 'UTIL_TEST_VAR') + + expect(resolution.value).to.equal('util-config-value') + expect(resolution.source).to.equal('booster-config-env') + expect(resolution.key).to.equal('UTIL_TEST_VAR') + }) + }) +}) + +describe('integration scenarios', () => { + let mockConfig: BoosterConfig + let originalEnv: typeof process.env + + beforeEach(() => { + ConfigurationService.reset() + mockConfig = new BoosterConfig('test') + originalEnv = { ...process.env } + }) + + afterEach(() => { + process.env = originalEnv + ConfigurationService.reset() + }) + + it('should implement correct precedence order', async () => { + // Set up all configuration sources + process.env['PRECEDENCE_TEST'] = 'env-value' + mockConfig.env['PRECEDENCE_TEST'] = 'config-value' + + const customProvider = new MockConfigurationProvider('custom', 25, { + PRECEDENCE_TEST: 'custom-value', + }) + mockConfig.addConfigurationProvider(customProvider) + + const instance = ConfigurationService.getInstance(mockConfig) + + // should resolve from highest priority (custom provider) + const value = await instance.getValue('PRECEDENCE_TEST') + expect(value).to.equal('custom-value') + + const resolution = await instance.resolve('PRECEDENCE_TEST') + expect(resolution.source).to.equal('custom') + }) + + it('should handle provider unavailability', async () => { + process.env['FALLBACK_TEST'] = 'env-fallback' + + const unavailableProvider = new MockConfigurationProvider( + 'unavailable', + 25, + { FALLBACK_TEST: 'unavailable-value' }, + false // Not available + ) + mockConfig.addConfigurationProvider(unavailableProvider) + + const instance = ConfigurationService.getInstance(mockConfig) + const value = await instance.getValue('FALLBACK_TEST') + + // Should fallback to environment variables + expect(value).to.equal('env-fallback') + }) + + it('should handle multiple custom providers with correct priority', async () => { + const lowProvider = new MockConfigurationProvider('low', 15, { MULTI_TEST: 'low-value' }) + const highProvider = new MockConfigurationProvider('high', 25, { MULTI_TEST: 'high-value' }) + const mediumProvider = new MockConfigurationProvider('mid', 20, { MULTI_TEST: 'medium-value' }) + + mockConfig.addConfigurationProvider(lowProvider) + mockConfig.addConfigurationProvider(highProvider) + mockConfig.addConfigurationProvider(mediumProvider) + + const instance = ConfigurationService.getInstance(mockConfig) + const value = await instance.getValue('MULTI_TEST') + + expect(value).to.equal('high-value') + }) +}) diff --git a/packages/framework-types/src/config.ts b/packages/framework-types/src/config.ts index fc2a8e8e4..d1fa7130f 100644 --- a/packages/framework-types/src/config.ts +++ b/packages/framework-types/src/config.ts @@ -203,6 +203,9 @@ export class BoosterConfig { /** Environment variables set at deployment time on the target lambda functions */ public readonly env: Record = {} + /** Configuration providers for external configuration sources (Azure App Configuration, etc.) **/ + public readonly configurationProviders: ConfigurationProvider[] = [] + /** * Add `TokenVerifier` implementations to this array to enable token verification. * When a bearer token arrives in a request 'Authorization' header, it will be checked @@ -261,6 +264,43 @@ export class BoosterConfig { this.validateAllMigrations() } + /** + * Register a configuration provider for external configuration sources + * @param provider The configuration provider to register + */ + public addConfigurationProvider(provider: ConfigurationProvider): void { + // Remove any existing provider with the same name + const existingIndex = this.configurationProviders.findIndex((p) => p.name === provider.name) + if (existingIndex >= 0) { + this.configurationProviders.splice(existingIndex, 1) + } + + // Add the new provider and sort by priority (highest first) + this.configurationProviders.push(provider) + this.configurationProviders.sort((a, b) => b.priority - a.priority) + } + + /** + * Enable Azure App Configuration for this environment + * This is a convenience method that automatically configures the Azure App Configuration provider + * @param options Configuration options for Azure App Configuration + */ + public enableAzureAppConfiguration(options?: { + connectionString?: string + endpoint?: string + labelFilter?: string + }): void { + // This method signature needs to remain in framework-types, but the actual implementation + // will be provided by the Azure provider package to avoid circular dependencies + // Store the options in a special property that the Azure provider can read + ;(this as any)._azureAppConfigOptions = { + connectionString: options?.connectionString, + endpoint: options?.endpoint, + labelFilter: options?.labelFilter, + enabled: true, + } + } + public get provider(): ProviderLibrary { if (!this._provider && this.providerPackage) { const rockets = this.rockets ?? [] From 4af9fcd8cad80e145a4d6d9f7cc360c3736dec38 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Mon, 4 Aug 2025 16:30:42 -0400 Subject: [PATCH 03/23] Adds configuration adapter to Azure provider package --- .../framework-provider-azure/package.json | 1 + .../framework-provider-azure/src/index.ts | 25 ++++ .../src/library/configuration-adapter.ts | 117 +++++++++++++++ .../library/configuration-adapter.test.ts | 141 ++++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 packages/framework-provider-azure/src/library/configuration-adapter.ts create mode 100644 packages/framework-provider-azure/test/library/configuration-adapter.test.ts diff --git a/packages/framework-provider-azure/package.json b/packages/framework-provider-azure/package.json index 32842c704..e3762ccbf 100644 --- a/packages/framework-provider-azure/package.json +++ b/packages/framework-provider-azure/package.json @@ -23,6 +23,7 @@ "node": ">=20.0.0 <21.0.0" }, "dependencies": { + "@azure/app-configuration": "^1.7.0", "@azure/cosmos": "^4.3.0", "@azure/functions": "^1.2.2", "@azure/identity": "~4.7.0", diff --git a/packages/framework-provider-azure/src/index.ts b/packages/framework-provider-azure/src/index.ts index bcfc72402..7958e32ef 100644 --- a/packages/framework-provider-azure/src/index.ts +++ b/packages/framework-provider-azure/src/index.ts @@ -49,6 +49,7 @@ import { } from './library/health-adapter' import { deleteEvent, deleteSnapshot, findDeletableEvent, findDeletableSnapshot } from './library/event-delete-adapter' import { storeEvents } from './library/events-store-adapter' +import { ConfigurationAdapter } from './library/configuration-adapter' let cosmosClient: CosmosClient if (typeof process.env[environmentVarNames.cosmosDbConnectionString] === 'undefined') { @@ -115,6 +116,29 @@ export function loadInfrastructurePackage(packageName: string): HasInfrastructur return require(packageName) } +/** + * Initialize Azure App Configuration adapter if configured + */ +function initializeAzureAppConfiguration(config: any): void { + const azureAppConfigOptions = config._azureAppConfigOptions + if (azureAppConfigOptions?.enabled) { + const adapter = new ConfigurationAdapter( + azureAppConfigOptions.connectionString, + azureAppConfigOptions.endpoint, + azureAppConfigOptions.labelFilter + ) + config.addConfigurationProvider(adapter) + } +} + +/** + * Setup Azure App Configuration for the given Booster configuration + * This function should be called during configuration setup to enable Azure App Configuration + */ +export function setupAzureAppConfiguration(config: any): void { + initializeAzureAppConfiguration(config) +} + export const Provider = (rockets?: RocketDescriptor[]): ProviderLibrary => ({ // ProviderEventsLibrary events: { @@ -201,3 +225,4 @@ export const Provider = (rockets?: RocketDescriptor[]): ProviderLibrary => ({ }) export * from './constants' +export * from './library/configuration-adapter' diff --git a/packages/framework-provider-azure/src/library/configuration-adapter.ts b/packages/framework-provider-azure/src/library/configuration-adapter.ts new file mode 100644 index 000000000..2460691c0 --- /dev/null +++ b/packages/framework-provider-azure/src/library/configuration-adapter.ts @@ -0,0 +1,117 @@ +import { ConfigurationProvider } from '@boostercloud/framework-types' +import { AppConfigurationClient } from '@azure/app-configuration' +import { DefaultAzureCredential } from '@azure/identity' + +export class ConfigurationAdapter implements ConfigurationProvider { + readonly name = 'azure-app-configuration' + readonly priority = 20 // High priority - external configuration source + + private client: AppConfigurationClient | undefined + private isInitialized = false + private initializationError: Error | undefined + + constructor( + private readonly connectionString?: string, + private readonly endpoint?: string, + private readonly labelFilter?: string + ) {} + + /** + * Initialize the Azure App Configuration client + */ + private async initialize(): Promise { + if (this.isInitialized) { + return + } + + try { + if (this.connectionString) { + // Use connection string if provided + this.client = new AppConfigurationClient(this.connectionString) + } else if (this.endpoint) { + // Use managed identity or default Azure credential with endpoint + const credential = new DefaultAzureCredential() + this.client = new AppConfigurationClient(this.endpoint, credential) + } else { + throw new Error('Azure App Configuration requires either a connection string or endpoint URL') + } + } catch (error) { + this.initializationError = error instanceof Error ? error : new Error(String(error)) + this.isInitialized = true // Mark as initialized to avoid retrying + throw this.initializationError + } + } + + async getValue(key: string): Promise { + try { + await this.initialize() + + if (!this.client) { + return undefined + } + + // Get the configuration setting with optional label filter + const configurationSetting = await this.client.getConfigurationSetting({ + key, + label: this.labelFilter, + }) + + return configurationSetting.value + } catch (error) { + // Log the error but don't throw - this allows fallback to other providers + console.warn(`Azure App Configuration failed to get the value for key '${key}':`, error) + return undefined + } + } + + async isAvailable(): Promise { + try { + await this.initialize() + return !!this.client && !this.initializationError + } catch (error) { + return false + } + } + + /** + * Create a ConfigurationAdapter instance from environment variables + * this is the standard way to initialize the provider in Azure environments + */ + static fromEnvironment(labelFilter?: string): ConfigurationAdapter { + const connectionString = process.env['AZURE_APP_CONFIG_CONNECTION_STRING'] + const endpoint = process.env['AZURE_APP_CONFIG_ENDPOINT'] + + return new ConfigurationAdapter(connectionString, endpoint, labelFilter) + } + + /** + * Create a ConfigurationAdapter instance with connection string + */ + static withConnectionString(connectionString: string, labelFilter?: string): ConfigurationAdapter { + return new ConfigurationAdapter(connectionString, undefined, labelFilter) + } + + /** + * Create a ConfigurationAdapter instance with endpoint and managed identity + */ + static withEndpoint(endpoint: string, labelFilter?: string): ConfigurationAdapter { + return new ConfigurationAdapter(undefined, endpoint, labelFilter) + } +} + +/** + * Configuration options for Azure App Configuration integration + */ +export interface AzureAppConfigurationOptions { + /** Connection string for Azure App Configuration (alternative to endpoint + managed identity) */ + connectionString?: string + + /** Endpoint URL for Azure App Configuration (used with managed identity) */ + endpoint?: string + + /** Optional label filter to target specific configuration values */ + labelFilter?: string + + /** Whether to enable Azure App Configuration (default: false) */ + enabled?: boolean +} diff --git a/packages/framework-provider-azure/test/library/configuration-adapter.test.ts b/packages/framework-provider-azure/test/library/configuration-adapter.test.ts new file mode 100644 index 000000000..23c323e5a --- /dev/null +++ b/packages/framework-provider-azure/test/library/configuration-adapter.test.ts @@ -0,0 +1,141 @@ +import { restore, stub } from 'sinon' +import { ConfigurationAdapter } from '../../src' +import { expect } from '../expect' + +describe('ConfigurationAdapter', () => { + beforeEach(() => { + // Silence console warning during tests to avoid clutter + stub(console, 'warn') + }) + + afterEach(() => { + restore() + }) + + describe('constructor', () => { + it('should create an instance with connection string', () => { + const adapter = new ConfigurationAdapter('mock-connection-string') + expect(adapter.name).to.equal('azure-app-configuration') + expect(adapter.priority).to.equal(20) + }) + + it('should create an instance with endpoint', () => { + const adapter = new ConfigurationAdapter(undefined, 'https://mock-endpoint.azconfig.io') + expect(adapter.name).to.equal('azure-app-configuration') + expect(adapter.priority).to.equal(20) + }) + + it('should create an instance with label filter', () => { + const adapter = new ConfigurationAdapter('mock-connection-string', undefined, 'test-label') + expect(adapter.name).to.equal('azure-app-configuration') + expect(adapter.priority).to.equal(20) + }) + }) + + describe('static factory methods', () => { + beforeEach(() => { + // Clear environment variables + delete process.env['AZURE_APP_CONFIG_CONNECTION_STRING'] + delete process.env['AZURE_APP_CONFIG_ENDPOINT'] + }) + + afterEach(() => { + // Restore environment variables + delete process.env['AZURE_APP_CONFIG_CONNECTION_STRING'] + delete process.env['AZURE_APP_CONFIG_ENDPOINT'] + }) + + it('should create adapter from environment with connection string', () => { + process.env['AZURE_APP_CONFIG_CONNECTION_STRING'] = 'mock-connection-string' + + const adapter = ConfigurationAdapter.fromEnvironment() + expect(adapter.name).to.equal('azure-app-configuration') + }) + + it('should create adapter from environment with endpoint', () => { + process.env['AZURE_APP_CONFIG_ENDPOINT'] = 'https://mock-endpoint.azconfig.io' + + const adapter = ConfigurationAdapter.fromEnvironment() + expect(adapter.name).to.equal('azure-app-configuration') + }) + + it('should create adapter with connection string', () => { + const adapter = ConfigurationAdapter.withConnectionString('mock-connection-string') + expect(adapter.name).to.equal('azure-app-configuration') + }) + + it('should create adapter with endpoint', () => { + const adapter = ConfigurationAdapter.withEndpoint('https://mock-endpoint.azconfig.io') + expect(adapter.name).to.equal('azure-app-configuration') + }) + }) + + describe('isAvailable', () => { + it('should return false when no connection string or endpoint is provided', async () => { + const adapter = new ConfigurationAdapter() + const available = await adapter.isAvailable() + expect(available).to.be.false + }) + + it('should return false when initialization fails', async () => { + // Create provider with invalid connection string to force initialization error + const adapter = new ConfigurationAdapter('invalid-connection-string') + const available = await adapter.isAvailable() + expect(available).to.be.false + }) + }) + + describe('getValue', () => { + it('should return undefined when not available', async () => { + const adapter = new ConfigurationAdapter() + const value = await adapter.getValue('test-key') + expect(value).to.be.undefined + }) + + it('should return undefined when client fails', async () => { + const adapter = new ConfigurationAdapter('invalid-connection-string') + const value = await adapter.getValue('test-key') + expect(value).to.be.undefined + }) + }) + + describe('error handling', () => { + it('should handle initialization errors gracefully', async () => { + const adapter = new ConfigurationAdapter('invalid-connection-string') + + // Should not throw, even with invalid connection string + const available = await adapter.isAvailable() + expect(available).to.be.false + + const value = await adapter.getValue('test-key') + expect(value).to.be.undefined + }) + + it('should handle missing configuration gracefully', async () => { + const adapter = new ConfigurationAdapter() + + const available = await adapter.isAvailable() + expect(available).to.be.false + + const value = await adapter.getValue('nonexistent-key') + expect(value).to.be.undefined + }) + }) + + describe('integration scenarios', () => { + it('should work with label filters', async () => { + const adapter = new ConfigurationAdapter('mock-connection-string', undefined, 'producton') + + // Should create without throwing + expect(adapter.name).to.equal('azure-app-configuration') + expect(adapter.priority).to.equal(20) + }) + + it('should prefer connection string over endpoint', async () => { + const adapter = new ConfigurationAdapter('mock-connection-string', 'https://mock-endpoint.azconfig.io') + + // Should create without throwing + expect(adapter.name).to.equal('azure-app-configuration') + }) + }) +}) From 290071f5a1065a33ba02be5aebb76ee9f3442e56 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Mon, 4 Aug 2025 16:49:28 -0400 Subject: [PATCH 04/23] Adds Azure App Configuration-related code to Azure infra package --- .../infrastructure/synth/application-synth.ts | 9 +++ .../synth/terraform-app-configuration.ts | 75 +++++++++++++++++++ .../synth/terraform-function-app-settings.ts | 22 +++++- .../types/application-synth-stack.ts | 6 +- 4 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/application-synth.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/application-synth.ts index 3e53a1755..ded5e6319 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/application-synth.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/application-synth.ts @@ -35,6 +35,7 @@ import { TerraformSubnetSecurity } from './gateway/terraform-subnet-security' import { BASIC_SERVICE_PLAN } from '../constants' import { TerraformFunctionAppSettings } from './terraform-function-app-settings' import { configuration } from '../helper/params' +import { TerraformAppConfiguration } from './terraform-app-configuration' export class ApplicationSynth { readonly config: BoosterConfig @@ -93,6 +94,7 @@ export class ApplicationSynth { stack.cosmosdbDatabase = TerraformCosmosdbDatabase.build(stack) stack.cosmosdbSqlDatabase = TerraformCosmosdbSqlDatabase.build(stack, this.config) stack.containers = TerraformContainers.build(stack, this.config) + this.buildAppConfiguration(stack) this.buildEventHub(stack) this.buildWebPubSub(stack) if (BASIC_SERVICE_PLAN === 'true') { @@ -132,6 +134,13 @@ export class ApplicationSynth { ) } + private buildAppConfiguration(stack: ApplicationSynthStack): void { + if (TerraformAppConfiguration.isEnabled(this.config)) { + const appConfigResource = new TerraformAppConfiguration(stack.terraformStack, stack, this.config) + stack.appConfiguration = appConfigResource.appConfiguration + } + } + private buildEventHub(stack: ApplicationSynthStack): void { if (this.config.eventStreamConfiguration.enabled) { stack.eventHubNamespace = TerraformEventHubNamespace.build(stack) diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts new file mode 100644 index 000000000..7658e88bc --- /dev/null +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts @@ -0,0 +1,75 @@ +import { Construct } from 'constructs' +import { appConfiguration } from '@cdktf/provider-azurerm' +import { BoosterConfig } from '@boostercloud/framework-types' +import { ApplicationSynthStack } from '../types/application-synth-stack' +import { toTerraformName } from '../helper/utils' + +export class TerraformAppConfiguration extends Construct { + public readonly appConfiguration: appConfiguration.AppConfiguration + + constructor(scope: Construct, applicationStack: ApplicationSynthStack, config: BoosterConfig) { + super(scope, 'AppConfiguration') + + const { appPrefix, resourceGroup } = applicationStack + + // Check if Azure App Configuration is enabled in the config + const azureAppConfigOptions = (config as any)._azureAppConfigOptions + if (!azureAppConfigOptions?.enabled) { + // If not eabled, create a placeholder without actual resources + this.appConfiguration = {} as appConfiguration.AppConfiguration + return + } + + const name = toTerraformName(appPrefix, 'appconfig') + + this.appConfiguration = new appConfiguration.AppConfiguration(this, 'AppConfiguration', { + name, + resourceGroupName: resourceGroup.name, + location: resourceGroup.location, + sku: 'free', // Use free tier by default + tags: { + Application: config.appName, + Environment: config.environmentName, + BoosterManaged: 'true', + }, + // Enable managed identity for secure access + identity: { + type: 'SystemAssigned', + }, + // Configure public network access + publicNetworkAccess: 'Enabled', + // Configure local authentication + localAuthEnabled: true, + }) + } + + /** + * Get the connection string for the App Configuration resource + */ + public getConnectionString(): string { + if (!this.appConfiguration || !this.appConfiguration.primaryWriteKey) { + return '' + } + return `Endpoint=https://${this.appConfiguration.name}.azconfig.io;Id=${ + this.appConfiguration.primaryWriteKey.get(0).id + };Secret=${this.appConfiguration.primaryWriteKey.get(0).secret}` + } + + /** + * Get the endpoint URL for the App Configuration resource + */ + public getEndpoint(): string { + if (!this.appConfiguration || !this.appConfiguration.endpoint) { + return '' + } + return this.appConfiguration.endpoint + } + + /** + * Check if App Configuration is enabled for this configuration + */ + public static isEnabled(config: BoosterConfig): boolean { + const azureAppConfigOptions = (config as any)._azureAppConfigOptions + return azureAppConfigOptions?.enabled === true + } +} diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts index 54907f464..9ed3639c3 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts @@ -6,7 +6,15 @@ import { storageAccount } from '@cdktf/provider-azurerm' export class TerraformFunctionAppSettings { static build( - { appPrefix, cosmosdbDatabase, domainNameLabel, eventHubNamespace, eventHub, webPubSub }: ApplicationSynthStack, + { + appPrefix, + cosmosdbDatabase, + domainNameLabel, + eventHubNamespace, + eventHub, + webPubSub, + appConfiguration, + }: ApplicationSynthStack, config: BoosterConfig, storageAccount: storageAccount.StorageAccount, suffixName: string @@ -20,6 +28,15 @@ export class TerraformFunctionAppSettings { ? `${eventHubNamespace.defaultPrimaryConnectionString};EntityPath=${eventHub.name}` : '' const region = (process.env['REGION'] ?? '').toLowerCase().replace(/ /g, '') + + // Azure App Configuration settings + const appConfigConnectionString = appConfiguration?.primaryWriteKey + ? `Endpoint=https://${appConfiguration.name}.azconfig.io;Id=${ + appConfiguration.primaryWriteKey.get(0).id + };Secret=${appConfiguration.primaryWriteKey.get(0).secret}` + : '' + const appConfigEndpoint = appConfiguration?.endpoint || '' + return { WEBSITE_RUN_FROM_PACKAGE: '1', WEBSITE_CONTENTSHARE: id, @@ -35,6 +52,9 @@ export class TerraformFunctionAppSettings { COSMOSDB_CONNECTION_STRING: `AccountEndpoint=https://${cosmosdbDatabase.name}.documents.azure.com:443/;AccountKey=${cosmosdbDatabase.primaryKey};`, WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: storageAccount.primaryConnectionString, // Terraform bug: https://github.com/hashicorp/terraform-provider-azurerm/issues/16650 BOOSTER_APP_NAME: process.env['BOOSTER_APP_NAME'] ?? '', + // Azure App Configuration settings + AZURE_APP_CONFIG_CONNECTION_STRING: appConfigConnectionString, + AZURE_APP_CONFIG_ENDPOINT: appConfigEndpoint, } } } diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/types/application-synth-stack.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/types/application-synth-stack.ts index 8da66e7ff..de1cca670 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/types/application-synth-stack.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/types/application-synth-stack.ts @@ -1,5 +1,6 @@ import { apiManagementApi, + appConfiguration, applicationGateway, cosmosdbAccount, cosmosdbSqlContainer, @@ -13,10 +14,10 @@ import { resourceGroup, servicePlan, storageAccount, - virtualNetwork, - webPubsub, subnet, subnetNetworkSecurityGroupAssociation, + virtualNetwork, + webPubsub, webPubsubHub, windowsFunctionApp, } from '@cdktf/provider-azurerm' @@ -62,5 +63,6 @@ export interface ApplicationSynthStack extends StackNames { consumerFunctionDefinitions?: Array eventHubNamespace?: eventhubNamespace.EventhubNamespace eventHub?: eventhub.Eventhub + appConfiguration?: appConfiguration.AppConfiguration rocketStack?: Array } From 662c51c33143008e265249617f7e92d086dc7a05 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Tue, 5 Aug 2025 15:48:33 -0400 Subject: [PATCH 05/23] Fixes initialization of App Configuration --- .../framework-provider-azure/src/constants.ts | 2 + .../framework-provider-azure/src/index.ts | 41 ++++++++----------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/framework-provider-azure/src/constants.ts b/packages/framework-provider-azure/src/constants.ts index 428c72d74..8f5f3c951 100644 --- a/packages/framework-provider-azure/src/constants.ts +++ b/packages/framework-provider-azure/src/constants.ts @@ -37,6 +37,8 @@ export const environmentVarNames = { eventHubMode: 'EVENTHUB_MODE', rocketFunctionAppNames: 'ROCKET_FUNCTION_APP_NAMES', rocketPackageMapping: 'ROCKET_PACKAGE_MAPPING', + appConfigurationConnectionString: 'AZURE_APP_CONFIG_CONNECTION_STRING', + appConfigurationEndpoint: 'AZURE_APP_CONFIG_ENDPOINT', } as const // Azure special error codes diff --git a/packages/framework-provider-azure/src/index.ts b/packages/framework-provider-azure/src/index.ts index 7958e32ef..46f2ccd9f 100644 --- a/packages/framework-provider-azure/src/index.ts +++ b/packages/framework-provider-azure/src/index.ts @@ -108,6 +108,24 @@ if ( }) } +const azureAppConfigConnectionString = process.env[environmentVarNames.appConfigurationConnectionString] +const azureAppConfigEndpoint = process.env[environmentVarNames.appConfigurationEndpoint] + +if (azureAppConfigConnectionString || azureAppConfigEndpoint) { + try { + const config = require('@boostercloud/framework-core').Booster.config + + const provider = ConfigurationAdapter.fromEnvironment() + config.addConfigurationProvider(provider) + } catch (error) { + console.warn('[Azure Provider] Failed to initialize Azure App Configuration adapter:', error) + } +} else { + console.warn( + '[Azure Provider] No Azure App Configuration connection string or endpoint found. The configuration adapter will not be available.', + ) +} + /* We load the infrastructure package dynamically here to avoid including it in the * dependencies that are deployed in the lambda functions. The infrastructure * package is only used during the deploy. @@ -116,29 +134,6 @@ export function loadInfrastructurePackage(packageName: string): HasInfrastructur return require(packageName) } -/** - * Initialize Azure App Configuration adapter if configured - */ -function initializeAzureAppConfiguration(config: any): void { - const azureAppConfigOptions = config._azureAppConfigOptions - if (azureAppConfigOptions?.enabled) { - const adapter = new ConfigurationAdapter( - azureAppConfigOptions.connectionString, - azureAppConfigOptions.endpoint, - azureAppConfigOptions.labelFilter - ) - config.addConfigurationProvider(adapter) - } -} - -/** - * Setup Azure App Configuration for the given Booster configuration - * This function should be called during configuration setup to enable Azure App Configuration - */ -export function setupAzureAppConfiguration(config: any): void { - initializeAzureAppConfiguration(config) -} - export const Provider = (rockets?: RocketDescriptor[]): ProviderLibrary => ({ // ProviderEventsLibrary events: { From 6750e8efbde3e7f0c78d91bff18be69d2fee01a1 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Wed, 6 Aug 2025 16:54:33 -0400 Subject: [PATCH 06/23] Updates documentation --- website/docs/03_features/03_configuration.mdx | 401 ++++++++++++++++++ .../environment-configuration.mdx | 13 +- .../infrastructure-providers.mdx | 90 ++++ website/sidebars.js | 3 +- 4 files changed, 505 insertions(+), 2 deletions(-) create mode 100644 website/docs/03_features/03_configuration.mdx diff --git a/website/docs/03_features/03_configuration.mdx b/website/docs/03_features/03_configuration.mdx new file mode 100644 index 000000000..84150fbc2 --- /dev/null +++ b/website/docs/03_features/03_configuration.mdx @@ -0,0 +1,401 @@ +import TerminalWindow from '@site/src/components/TerminalWindow/TerminalWindow' + +# Configuration Management + +Booster provides a flexible configuration management system that allows you to retrieve configuration values from multiple sources with automatic fallback mechanisms. This enables you to manage aplication settings dynamically without code changes or redeployments. + +## Configuration Resolution Hierarchy + +Booster resolves configuration values using a 3-tier priority system (highest to lowest priority): + +1. **External Configuration Providers** - Azure App Configuration, custom providers (highest priority) +2. **Booster config.env** - Configuration defined in your Booster config +3. **System Environment Variables** - Standard process.env values (lowest priority) + +When you request a configuration value, Booster checks each source in order and returns the first value found. + +```typescript +import { Booster } from '@boostercloud/framework-core' +import { resolveConfigurationWithSource } from '@boostercloud/framework-core' + +// Resolve a configuration value with source tracking +const resolution = await resolveConfigurationWithSource(Booster.config, 'API_TIMEOUT') +console.log(`Value: ${resolution.value}, Source: ${resolution.source}`) +``` + +## Built-in Configuration Providers + +### Environment Variables Provider + +Automatically reads values from `process.env`. This is always available and has the lowest priority. + +```typescript +// Will read from process.env.DATABASE_URL +const dbUrl = await resolveConfigurationValue(Booster.config, 'DATABASE_URL') +``` + +### Booster Config Environment Provider + +Reads values from the `config.env` object defined in your Booster configuration: + +```typescript title="src/config/config.ts" +Booster.configure('production', (config: BoosterConfig): void =>{ + config.appName = 'my-app' + config.providerPackage = '@boostercloud/framework-provider-azure' + + // Define configuration values + config.env = { + 'API_TIMEOUT': '5000', + 'MAX_RETRIES': '3', + 'FEATURE_FLAG_X': 'enabled', + } +}) +``` + +## Azure App Configuration + +Azure App Configuration provides centralized configuration management for cloud applications. It allows you to: + +- **Update configuration without redeployment** - Change values in Azure portal and they're immediately available +- **Feature flags and A/B testing** - Dynamic feature management +- **Environment-specific configuration** - Different values per environment using labels +- **Secure configuration storage** - Integration with Azure Key Vault for secrets + +### Enabling Azure App Configuration + +To enable Azure App Configuration for your Booster application: + +```typescript title="src/config/config.ts" +import { Booster } from '@boostercloud/framework-core' +import { BoosterConfig } from '@boostercloud/framework-types' + +Booster.configure('production', (config: BoosterConfig): void => { + config.appName = 'my-app' + config.providerPackage = '@boostercloud/framework-provider-azure' + + // Enable Azure App Configuration + config.enableAzureAppConfiguration() +}) +``` + +### Advanced Azure App Configuration Options + +You can customize the Azure App Configuration integration: + +```typescript title="src/config/config.ts" +Booster.configure('production', (config: BoosterConfig): void => {) + config.appName = 'my-app' + config.providerPackage = '@boostercloud/framework-provider-azure' + + // Advanced configuration + config.enableAzureAppConfiguration({ + // Optional: Override connection string (usually set by infrastructure + connectionString: 'Endpoint=https://myappconfig.azconfig.io;Id=xxx;Secret=xxx', + + // Optional: Override endpoint (alternative to connection string) + endpoint: 'https://myappconfig.azconfig.io', + + // Optional: Filter by label (for environment-specific values) + labelFilter: 'production', + }) +}) +``` + +### Using Labels for Environment Isolation + +Labels are a powerful feature of Azure App Configuration that allow you to maintain different configuration values for different environments while using the same keys. Here's how to set this up: + +#### 1. Configure Different Environments with Labels + +```typescript title="src/config/config.ts" +// Development environment - uses 'development' label +Booster.configure('development', (config: BoosterConfig): void => { + config.appName = 'my-app-dev' + config.providerPackage = '@boostercloud/framework-provider-azure' + + config.enableAzureAppConfiguration({ + labelFilter: 'development', // Only reads keys with 'development' label + }) +}) + +// Staging environment - uses 'staging' label +Booster.configure('staging', (config: BoosterConfig): void => { + config.appName = 'my-app-staging' + config.providerPackage = '@boostercloud/framework-provider-azure' + + config.enableAzureAppConfiguration({ + labelFilter: 'staging', // Only reads keys with 'staging' label + }) +}) + +// Production environment - uses 'production' label +Booster.configure('production', (config: BoosterConfig): void => { + config.appName = 'my-app-prod' + config.providerPackage = '@boostercloud/framework-provider-azure' + + config.enableAzureAppConfiguration({ + labelFilter: 'production', // Only reads keys with 'production' label + }) +}) +``` + +#### 2. Azure App Configuration Setup with Labels + +In your Azure App Configuration resource, create the same keys with different labels: + +| Key | Label | Value | Use Case | +|-----|-------|-------|----------| +| `API_TIMEOUT` | `development` | `5000` | Fast timeout for dev | +| `API_TIMEOUT` | `staging` | `15000` | Medium timeout for staging | +| `API_TIMEOUT` | `production` | `30000` | Longer timeout for prod | +| `DEBUG_MODE` | `development` | `enabled` | Debug logs in dev | +| `DEBUG_MODE` | `staging` | `enabled` | Debug logs in staging | +| `DEBUG_MODE` | `production` | `disabled` | No debug logs in prod | +| `FEATURE_NEW_CHECKOUT` | `development` | `enabled` | Test new features | +| `FEATURE_NEW_CHECKOUT` | `staging` | `enabled` | QA testing | +| `FEATURE_NEW_CHECKOUT` | `production` | `disabled` | Stable prod version | + +#### 3. Using Labeled Configuration in Your Code + +Your application code remains the same - the label filtering is handled automatically: + +```typescript title="src/commands/process-payment.ts" +@Command({ + authorize: 'all', +}) +export class ProcessPayment { + public constructor(readonly amount: number) {} + + public static async handle(command: ProcessPayment): Promise { + // This will get different values based on the environment's label filter: + // - development: 5000ms + // - staging: 15000ms + // - production: 30000ms + const timeoutMs = await resolveConfigurationValue(Booster.config, 'API_TIMEOUT') || '10000' + + // This will show debug info only in dev/staging, not production + const debugMode = await resolveConfigurationValue(Booster.config, 'DEBUG_MODE') + + if (debugMode === 'enabled') { + console.log(`Processing payment of ${command.amount} with timeout ${timeoutMs}ms`) + } + + // Your payment processing logic here... + return `Payment processed with ${timeoutMs}ms timeout` + } +} +``` + +#### 4. Benefits of Label-Based Environment Isolation + +- **🔧 Same Codebase**: No code changes needed between environments +- **🔁 Easy Promotion**: Promote the same code through dev → staging → prod +- **🎯 Environment-Specific Tuning**: Different timeouts, feature flags, etc. per environment +- **🛡️ Safety**: Production values are isolated from development changes +- **📊 A/B Testing**: Use different labels for different user groups in the same environment + +#### 5. Advanced Label Usage Examples + +**Feature Flag Rollout by Environment:** +```typescript +// Gradually roll out new features +const useNewPaymentFlow = await resolveConfigurationValue(Booster.config, 'NEW_PAYMENT_FLOW') +// development: 'enabled' - test new flow +// staging: 'enabled' - QA the new flow +// production: 'disabled' - keep stable flow until ready +``` + +**Environment-Specific Integrations:** +```typescript +// Different API endpoints per environment +const paymentUrl = await resolveConfigurationValue(Booster.config, 'PAYMENT_API_URL') +// development: 'https://sandbox-payments.example.com' +// staging: 'https://staging-payments.example.com' +// production: 'https://api-payments.example.com' +``` + +:::note +In most cases, you don't need to specify connection strings or endpoints manually. The Booster Azure provider automatically provisions the Azure App Configuration resource and injects the connection details as environment variables during deployment. +::: + +### Infrastructure Provisioning + +When you enable Azure App Configuration, the Booster Azure provider automatically: + +1. **Creates the Azure App Configuration resource** using Terraform +2. **Sets up authentication** with managed identity and access keys +3. **Injects environment variables** into your Function App: + - `AZURE_APP_CONFIG_CONNECTION_STRING` + - `AZURE_APP_CONFIG_ENDPOINT` +4. **Initializes the configuration provider** at runtime when environment variables are available + +## Creating Custom Configuration Providers + +You can implement custom configuration providers for other external systems: + +```typescript title="src/providers/custom-config-provider.ts" +import { ConfigurationProvider } from '@boostercloud/framework-types' + +export class CustomConfigurationProvider implements ConfigurationProvider { + public readonly name = 'CustomProvider' + public readonly priority = 15 // Between Azure App Config (20) and Booster config.env (10) + + constructor(private apiEndpoint: string, private apiKey: string) {} + + async getValue(key: string): Promise { + try { + // Implement your custom logic to fetch configuration + const response = await fetch(`${this.apiEndpoint}/config/${key}`, { + headers: { 'Authorization': `Bearer ${this.apiKey}` }, + }) + + if (response.ok) { + const data = await response.json() + return data.value + } + } catch (error) { + console.warn(`Custom provider failed to get ${key}:`, error) + } + + return undefined + } + + async isAvailable(): Promise { + // Check if the service is reachable + try { + const response = await fetch(`${this.apiEndpoint}/health`) + return response.ok + } catch { + return false + } + } +} +``` + +Register your custom provider in the configuration: + +```typescript title="src/config/config.ts" +import { CustomConfigurationProvider } from '../providers/custom-config-provider' + +Booster.configure('production', (config: BoosterConfig): void => {) + config.appName = 'my-app' + config.providerPackage = '@boostercloud/framework-provider-azure' + + // Add custom configuration provider + const customProvider = new CustomConfigurationProvider( + 'https://api.myservice.com', + process.env.CUSTOM_API_KEY, + ) + config.addConfigurationProvider(customProvider) +}) +``` + +## Best Practices + +### 1. Use Descriptive Configuration Keys + +```typescript +// Good +const timeout = await resolveConfigurationValue(Booster.config, 'PAYMENT_API_TIMEOUT_MS') + +// Avoid +const timeout = await resolveConfigurationValue(Booster.config, 'TIMEOUT') +``` + +### 2. Provide Sensible Defaults + +```typescript +const maxRetries = await resolveConfigurationValue(Booster.config, 'MAX_RETRIES') || '3' +const timeoutMs = parseInt( + await resolveConfigurationValue(Booster.config, 'API_TIMEOUT_MS') || '30000' +) +``` + +### 3. Use Source Tracking for Debugging + +```typescript +const resolution = await resolveConfigurationWithSource(Booster.config, 'DEBUG_MODE') +console.log(`Debug mode: ${resolution.value} (from ${resolution.source})`) +``` + +### 4. Handle Configuration Errors Gracefully + +```typescript +try { + const apiKey = await resolveConfigurationValue(Booster.config, 'EXTERNAL_API_KEY') + if (!apiKey) { + throw new Error('EXTERNAL_API_KEY configuration is required') + } + // Use apiKey... +} catch (error) { + console.error('Configuration error:', error) + // Provide fallback behavior or fail gracefully +} +``` + +### 5. Cache Configuration Values for Performance + +```typescript +class ConfigCache { + private cache = new Map() + private readonly TTL = 5 * 60 * 1000 // 5 minutes + + async get(key: string): Promise { + const cached = this.cache.get(key) + if (cached && Date.now() < cached.expiry) { + return cached.value + } + + const value = await resolveConfigurationValue(Booster.config, key) + if (value) { + this.cache.set(key, { value, expiry: Date.now() + this.TTL }) + } + return value + } +} +``` + +## Troubleshooting + +### Configuration Not Found + +If a configuration value is not found: + +```typescript +const resolution = await resolveConfigurationWithSource(Booster.config, 'MISSING_KEY') +console.log(resolution.source) // "none" +console.log(resolution.value) // undefined +``` + +### Azure App Configuration Connection Issues + +Check your environment variables are properly set: + + + ```shell + # In your deployed Function App, these should be automatically set: + echo $AZURE_APP_CONFIG_CONNECTION_STRING + echo $AZURE_APP_CONFIG_ENDPOINT + ``` + + +### Provider Priority Conflicts + +List all registered providers to debug priority issues: + +```typescript +const configService = ConfigurationService.getInstance() +const providers = configService.getProviders() + +providers.forEach((provider) => { + console.log(`${provider.name}: priority ${provider.priority}`) +}) +``` + +## Conclusion + +Booster's configuration management system provides a powerful, flexible way to manage application settings across different environments and sources. By leveraging external providers like Azure App Configuration, you can achieve true configuration-driven applications that adapt without requiring code changes or redeployments. + +The hierarchical resolution system ensures predicatable behavior while providing multiple layers of configuration sources for maximum flexibility and reliability. + + diff --git a/website/docs/10_going-deeper/environment-configuration.mdx b/website/docs/10_going-deeper/environment-configuration.mdx index 9d0591e84..ab9adce27 100644 --- a/website/docs/10_going-deeper/environment-configuration.mdx +++ b/website/docs/10_going-deeper/environment-configuration.mdx @@ -42,4 +42,15 @@ This way, you can have different configurations depending on your needs. Booster environments are extremely flexible. As shown in the first example, your 'fruit-store' app can have three team-wide environments: 'dev', 'stage', and 'prod', each of them with different app names or providers, that are deployed by your CI/CD processes. Developers, like "John" in the second example, can create their own private environments in separate config files to test their changes in realistic environments before committing them. Likewise, CI/CD processes could generate separate production-like environments to test different branches to perform QA in separate environments without interferences from other features under test. -The only thing you need to do to deploy a whole new completely-independent copy of your application is to use a different name. Also, Booster uses the credentials available in the machine (`~/.aws/credentials` in AWS) that performs the deployment process, so developers can even work on separate accounts than production or staging environments. \ No newline at end of file +The only thing you need to do to deploy a whole new completely-independent copy of your application is to use a different name. Also, Booster uses the credentials available in the machine (`~/.aws/credentials` in AWS) that performs the deployment process, so developers can even work on separate accounts than production or staging environments. + +## Advanced Configuration Management + +For managing dynamic configuration values that can be updated without redeployment, Booster provides a comprehensive configuration management system with support for external configuration providers like Azure App Configuration. + +See the [Configuration Management](/features/configuration) documentation for detailed information about: +- Multi-tier configuration resolutions +- Azure App Configuration integration +- Custom configuration providers +- Feature flags and A/B testing +- Dynamic configuration updates \ No newline at end of file diff --git a/website/docs/10_going-deeper/infrastructure-providers.mdx b/website/docs/10_going-deeper/infrastructure-providers.mdx index 6f83e8bdd..11c8ad6fc 100644 --- a/website/docs/10_going-deeper/infrastructure-providers.mdx +++ b/website/docs/10_going-deeper/infrastructure-providers.mdx @@ -327,6 +327,96 @@ Azure Provider will generate a default `host.json` file if there is not a `host. If you want to use your own `host.json` file just add it to `config.assets` array and Booster will use yours. +### Azure App Configuration Integration + +The Azure provider includes seamless integration with [Azure App Configuration service](https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview) for centralized configuration management, When enabled, Booster automatically provisions the necessary infrastructure and provides a unified configuration API. + +#### Enabling Azure App Configuration + +Add the following to your Azure environment configuration: + +```typescript +Booster.configure('production', (config: BoosterConfig): void => { + config.appName = 'my-app-name' + config.providerPackage = '@boostercloud/framework-provider-azure' + + // Enable Azure App Configuration + config.enableAzureAppConfiguration() +}) +``` + +#### Infrastructure Provisioning + +When you deploy with Azure App Configuration enabled, the Azure provider automatically> + +- **Creates an Azure App Configuration resource** in your resource group +- **Sets up authentication** using managed identity to and access keys +- **Configures environment variables** in your Function App: + - `AZURE_APP_CONFIG_CONNECTION_STRING`: Connection string for the App Configuration resource + - `AZURE_APP_CONFIG_ENDPOINT`: Endpoint URL for the App Configuration resource +- **Initializes the configuration provider** at runtime + +#### Using Configuration Values + +Once deployed, you can retrieve configuration avlues from any part of your application: + +```typescript +import { resolveConfigurationValue, resolveConfigurationWithSource } from '@boostercloud/framework-core' + +// Simple value resolution +const appTimeout = await resolveConfigurationValue(Booster.config, 'API_TIMEOUT' + +// Resolution with source tracking (useful for debugging) +const resolution = await resolveConfigurationWithSource(Booster.config, 'FEATURE_FLAG_X') +console.log(`${resolution.key}: ${resolution.value} (from ${resolution.source})`) +``` + +The configuration system uses a 3-tier priority hierarchy: +1. Azure App Configuration (highest priority) +2. Booster config.env values +3. System environment variables (lowest priority) + +#### Using Labels for Environment Isolation + +Configure different environments to use different label filters: + +```typescript +// Development environment +Booster.configure('development', (config: BoosterConfig): void => { + config.appName = 'my-app-dev' + config.providerPackage = '@boostercloud/framework-provider-azure' + + config.enableAzureAppConfiguration({ + labelFilter: 'development', // Reads only keys with 'development' label + }) +}) + +// Production environment +Booster.configure('production', (config: BoosterConfig): void => { + config.appName = 'my-app-prod' + config.providerPackage = '@boostercloud/framework-provider-azure' + + config.enableAzureAppConfiguration({ + labelFilter: 'production', // Reads only keys with 'production' label + }) +}) +``` + +In Azure App Configuration, create the same keys with different labels: +- `API_TIMEOUT` with label `development` set to `5000` +- `API_TIMEOUT` with label `production` set to `30000` + +Your code stays the same - the environment determines which value is retrieved. + +#### Benefits + +- **No redeployment required** - Update configuration values in Azure portal and they're immediately available +- **Environment isolation** - Use labels to manage different values per environment +- **Feature flags** - Enable/disable features dynamically +- **Secure configuration** - Integration with Azure Key Vault for sensitive values + +For detailed information about configuration management, see the [Configuration](/features/configuration) documentation. + ## Local Provider All Booster projects come with a local development environment configured by default, so you can test your app before deploying it to the cloud. diff --git a/website/sidebars.js b/website/sidebars.js index 2742979cc..074fd4ff3 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -53,7 +53,8 @@ const sidebars = { 'features/event-stream', 'features/schedule-actions', 'features/logging', - 'features/error-handling' + 'features/error-handling', + 'features/configuration', ], }, { From 911853840d19ea1d195c8b3450ec38416d846b9c Mon Sep 17 00:00:00 2001 From: Mario Castro Squella Date: Wed, 6 Aug 2025 17:28:25 -0400 Subject: [PATCH 07/23] Updates lock file --- common/config/rush/pnpm-lock.yaml | 131 ++++++++++++++++++------------ 1 file changed, 77 insertions(+), 54 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index bddaa69c6..3ad308541 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: specifier: 3.7.13 version: 3.7.13(graphql@16.10.0)(react@17.0.2)(subscriptions-transport-ws@0.11.0(graphql@16.10.0)) '@boostercloud/framework-types': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -47,7 +47,7 @@ importers: version: 8.18.0 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@types/jsonwebtoken': specifier: 9.0.8 @@ -104,10 +104,10 @@ importers: ../../packages/cli: dependencies: '@boostercloud/framework-core': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-core '@boostercloud/framework-types': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -150,10 +150,10 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/application-tester': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../application-tester '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@oclif/test': specifier: ^4.1.10 @@ -264,7 +264,7 @@ importers: ../../packages/framework-common-helpers: dependencies: '@boostercloud/framework-types': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -280,7 +280,7 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@types/chai': specifier: 4.2.18 @@ -370,10 +370,10 @@ importers: ../../packages/framework-core: dependencies: '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-common-helpers '@boostercloud/framework-types': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-types '@effect/cli': specifier: 0.56.2 @@ -437,10 +437,10 @@ importers: version: 8.18.0 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@boostercloud/metadata-booster': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../metadata-booster '@types/chai': specifier: 4.2.18 @@ -545,22 +545,22 @@ importers: ../../packages/framework-integration-tests: dependencies: '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-common-helpers '@boostercloud/framework-core': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-core '@boostercloud/framework-provider-aws': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-provider-aws '@boostercloud/framework-provider-azure': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-provider-azure '@boostercloud/framework-provider-local': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-provider-local '@boostercloud/framework-types': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -618,25 +618,25 @@ importers: specifier: 3.7.13 version: 3.7.13(graphql@16.10.0)(react@17.0.2)(subscriptions-transport-ws@0.11.0(graphql@16.10.0)) '@boostercloud/application-tester': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../application-tester '@boostercloud/cli': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../cli '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@boostercloud/framework-provider-aws-infrastructure': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-provider-aws-infrastructure '@boostercloud/framework-provider-azure-infrastructure': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-provider-azure-infrastructure '@boostercloud/framework-provider-local-infrastructure': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-provider-local-infrastructure '@boostercloud/metadata-booster': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../metadata-booster '@seald-io/nedb': specifier: 4.0.2 @@ -777,10 +777,10 @@ importers: ../../packages/framework-provider-aws: dependencies: '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-common-helpers '@boostercloud/framework-types': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -790,7 +790,7 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@types/aws-lambda': specifier: 8.10.48 @@ -943,13 +943,13 @@ importers: specifier: ^1.170.0 version: 1.204.0 '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-common-helpers '@boostercloud/framework-provider-aws': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-provider-aws '@boostercloud/framework-types': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -983,7 +983,7 @@ importers: version: 1.10.2 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@types/archiver': specifier: 5.1.0 @@ -1081,6 +1081,9 @@ importers: ../../packages/framework-provider-azure: dependencies: + '@azure/app-configuration': + specifier: ^1.7.0 + version: 1.9.0 '@azure/cosmos': specifier: ^4.3.0 version: 4.4.1 @@ -1097,10 +1100,10 @@ importers: specifier: ~1.1.0 version: 1.1.3 '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-common-helpers '@boostercloud/framework-types': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -1110,7 +1113,7 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@types/chai': specifier: 4.2.18 @@ -1203,16 +1206,16 @@ importers: specifier: ~4.7.0 version: 4.7.0 '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-common-helpers '@boostercloud/framework-core': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-core '@boostercloud/framework-provider-azure': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-provider-azure '@boostercloud/framework-types': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-types '@cdktf/provider-azurerm': specifier: 13.18.0 @@ -1279,7 +1282,7 @@ importers: version: 11.0.5 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@types/chai': specifier: 4.2.18 @@ -1360,10 +1363,10 @@ importers: ../../packages/framework-provider-local: dependencies: '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-common-helpers '@boostercloud/framework-types': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -1379,7 +1382,7 @@ importers: version: 8.18.0 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@types/chai': specifier: 4.2.18 @@ -1475,13 +1478,13 @@ importers: ../../packages/framework-provider-local-infrastructure: dependencies: '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-common-helpers '@boostercloud/framework-provider-local': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-provider-local '@boostercloud/framework-types': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -1500,7 +1503,7 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@types/chai': specifier: 4.2.18 @@ -1636,10 +1639,10 @@ importers: version: 8.18.0 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@boostercloud/metadata-booster': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../metadata-booster '@types/chai': specifier: 4.2.18 @@ -1733,7 +1736,7 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.0 + specifier: workspace:^3.4.1 version: link:../../tools/eslint-config '@types/node': specifier: ^20.17.17 @@ -2705,6 +2708,10 @@ packages: resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} engines: {node: '>=18.0.0'} + '@azure/app-configuration@1.9.0': + resolution: {integrity: sha512-X0AVDQygL4AGLtplLYW+W0QakJpJ417sQldOacqwcBQ882tAPdUVs6V3mZ4jUjwVsgr+dV1v9zMmijvsp6XBxA==} + engines: {node: '>=18.0.0'} + '@azure/arm-appservice@16.0.0': resolution: {integrity: sha512-oJBb1kpI6okJouyGKBqA9Kp1Em6CutdqbI+Q74pQz7ssv6zBoxIC9BCg15jvHOdK14JE16lbuf3nGqUZ6AyNbw==} engines: {node: '>=18.0.0'} @@ -7395,8 +7402,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@6.0.0-dev.20250801: - resolution: {integrity: sha512-b/X5+OCIRwL/sCMYpynN/Lkwx3H8Jnt+ttDIZo5bKWpYK1TTeh76tXoKsrUSex2dn8Sd8qqUf7OHifvdGmeKhg==} + typescript@6.0.0-dev.20250806: + resolution: {integrity: sha512-inDvi8ujsZXA/dSgj8QiSjHSi7fYDnkRck9vvnd400VBY5RSzNl6G3zZXjFZTawwYfETOakld5vQ6a36JuNOBQ==} engines: {node: '>=14.17'} hasBin: true @@ -8509,6 +8516,22 @@ snapshots: dependencies: tslib: 2.8.1 + '@azure/app-configuration@1.9.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.9.0 + '@azure/core-client': 1.9.2 + '@azure/core-http-compat': 2.3.0 + '@azure/core-lro': 2.7.2 + '@azure/core-paging': 1.6.2 + '@azure/core-rest-pipeline': 1.20.0 + '@azure/core-tracing': 1.2.0 + '@azure/core-util': 1.11.0 + '@azure/logger': 1.1.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@azure/arm-appservice@16.0.0': dependencies: '@azure/abort-controller': 2.1.2 @@ -10912,7 +10935,7 @@ snapshots: dependencies: semver: 7.6.3 shelljs: 0.8.5 - typescript: 6.0.0-dev.20250801 + typescript: 6.0.0-dev.20250806 dunder-proto@1.0.1: dependencies: @@ -14375,7 +14398,7 @@ snapshots: typescript@5.7.3: {} - typescript@6.0.0-dev.20250801: {} + typescript@6.0.0-dev.20250806: {} unbox-primitive@1.1.0: dependencies: From b20a67d53157ddac1a3cf08a2a76c03623ec1438 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Wed, 6 Aug 2025 17:35:32 -0400 Subject: [PATCH 08/23] Adds rush change file --- .../azure_app_configuration_2025-08-06-21-35.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@boostercloud/framework-core/azure_app_configuration_2025-08-06-21-35.json diff --git a/common/changes/@boostercloud/framework-core/azure_app_configuration_2025-08-06-21-35.json b/common/changes/@boostercloud/framework-core/azure_app_configuration_2025-08-06-21-35.json new file mode 100644 index 000000000..cd93677c4 --- /dev/null +++ b/common/changes/@boostercloud/framework-core/azure_app_configuration_2025-08-06-21-35.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@boostercloud/framework-core", + "comment": "Configuration provider with Azure App Configuration support", + "type": "minor" + } + ], + "packageName": "@boostercloud/framework-core" +} \ No newline at end of file From 6355f48b00e62389d205a437dba254faae98c862 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Wed, 6 Aug 2025 18:27:48 -0400 Subject: [PATCH 09/23] Fixes typo --- packages/framework-types/src/config.ts | 4 ++-- packages/framework-types/src/configuration-resolver.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/framework-types/src/config.ts b/packages/framework-types/src/config.ts index d1fa7130f..e0dd4a5ff 100644 --- a/packages/framework-types/src/config.ts +++ b/packages/framework-types/src/config.ts @@ -58,7 +58,7 @@ export interface ConfigurationProvider { /** * Configuration resolution result with source tracking */ -export interface ConfiguratonResolution { +export interface ConfigurationResolution { value: string | undefined source: string key: string @@ -73,7 +73,7 @@ export interface ConfigurationResolver { * @param key The configuration key to resolve * @returns Promise resolving to the configuration resolution result */ - resolve(key: string): Promise + resolve(key: string): Promise /** * Add a configuration provider diff --git a/packages/framework-types/src/configuration-resolver.ts b/packages/framework-types/src/configuration-resolver.ts index 889f88b12..a2448240c 100644 --- a/packages/framework-types/src/configuration-resolver.ts +++ b/packages/framework-types/src/configuration-resolver.ts @@ -1,4 +1,4 @@ -import { ConfigurationProvider, ConfigurationResolver, ConfiguratonResolution } from './config' +import { ConfigurationProvider, ConfigurationResolver, ConfigurationResolution } from './config' export class DefaultConfigurationResolver implements ConfigurationResolver { private providers: ConfigurationProvider[] = [] @@ -23,7 +23,7 @@ export class DefaultConfigurationResolver implements ConfigurationResolver { return [...this.providers] } - async resolve(key: string): Promise { + async resolve(key: string): Promise { // Try each provider in priority order for (const provider of this.providers) { try { From 5e7e06b4a5c31e5ab12272bc85647174068b50a1 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Wed, 6 Aug 2025 18:30:18 -0400 Subject: [PATCH 10/23] Fixes more typos --- .../src/infrastructure/synth/terraform-app-configuration.ts | 2 +- website/docs/03_features/03_configuration.mdx | 4 ++-- website/docs/10_going-deeper/infrastructure-providers.mdx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts index 7658e88bc..0086ce0d6 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts @@ -15,7 +15,7 @@ export class TerraformAppConfiguration extends Construct { // Check if Azure App Configuration is enabled in the config const azureAppConfigOptions = (config as any)._azureAppConfigOptions if (!azureAppConfigOptions?.enabled) { - // If not eabled, create a placeholder without actual resources + // If not enabled, create a placeholder without actual resources this.appConfiguration = {} as appConfiguration.AppConfiguration return } diff --git a/website/docs/03_features/03_configuration.mdx b/website/docs/03_features/03_configuration.mdx index 84150fbc2..b47173e90 100644 --- a/website/docs/03_features/03_configuration.mdx +++ b/website/docs/03_features/03_configuration.mdx @@ -2,7 +2,7 @@ import TerminalWindow from '@site/src/components/TerminalWindow/TerminalWindow' # Configuration Management -Booster provides a flexible configuration management system that allows you to retrieve configuration values from multiple sources with automatic fallback mechanisms. This enables you to manage aplication settings dynamically without code changes or redeployments. +Booster provides a flexible configuration management system that allows you to retrieve configuration values from multiple sources with automatic fallback mechanisms. This enables you to manage application settings dynamically without code changes or redeployments. ## Configuration Resolution Hierarchy @@ -396,6 +396,6 @@ providers.forEach((provider) => { Booster's configuration management system provides a powerful, flexible way to manage application settings across different environments and sources. By leveraging external providers like Azure App Configuration, you can achieve true configuration-driven applications that adapt without requiring code changes or redeployments. -The hierarchical resolution system ensures predicatable behavior while providing multiple layers of configuration sources for maximum flexibility and reliability. +The hierarchical resolution system ensures predictable behavior while providing multiple layers of configuration sources for maximum flexibility and reliability. diff --git a/website/docs/10_going-deeper/infrastructure-providers.mdx b/website/docs/10_going-deeper/infrastructure-providers.mdx index 11c8ad6fc..a40ff8ea9 100644 --- a/website/docs/10_going-deeper/infrastructure-providers.mdx +++ b/website/docs/10_going-deeper/infrastructure-providers.mdx @@ -347,10 +347,10 @@ Booster.configure('production', (config: BoosterConfig): void => { #### Infrastructure Provisioning -When you deploy with Azure App Configuration enabled, the Azure provider automatically> +When you deploy with Azure App Configuration enabled, the Azure provider automatically: - **Creates an Azure App Configuration resource** in your resource group -- **Sets up authentication** using managed identity to and access keys +- **Sets up authentication** using managed identity and access keys - **Configures environment variables** in your Function App: - `AZURE_APP_CONFIG_CONNECTION_STRING`: Connection string for the App Configuration resource - `AZURE_APP_CONFIG_ENDPOINT`: Endpoint URL for the App Configuration resource From 26f8f703ec4cd1d31f1aad980c68f49865d07d6f Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Wed, 6 Aug 2025 18:34:35 -0400 Subject: [PATCH 11/23] Fixes linting error --- packages/framework-provider-azure/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framework-provider-azure/src/index.ts b/packages/framework-provider-azure/src/index.ts index 46f2ccd9f..0f56f88ed 100644 --- a/packages/framework-provider-azure/src/index.ts +++ b/packages/framework-provider-azure/src/index.ts @@ -122,7 +122,7 @@ if (azureAppConfigConnectionString || azureAppConfigEndpoint) { } } else { console.warn( - '[Azure Provider] No Azure App Configuration connection string or endpoint found. The configuration adapter will not be available.', + '[Azure Provider] No Azure App Configuration connection string or endpoint found. The configuration adapter will not be available.' ) } From 67a4f6593d2b7ba6e145583297261261a361ccc3 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Wed, 6 Aug 2025 18:43:46 -0400 Subject: [PATCH 12/23] Removes unnecessary console.warn message --- packages/framework-provider-azure/src/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/framework-provider-azure/src/index.ts b/packages/framework-provider-azure/src/index.ts index 0f56f88ed..5958201be 100644 --- a/packages/framework-provider-azure/src/index.ts +++ b/packages/framework-provider-azure/src/index.ts @@ -120,10 +120,6 @@ if (azureAppConfigConnectionString || azureAppConfigEndpoint) { } catch (error) { console.warn('[Azure Provider] Failed to initialize Azure App Configuration adapter:', error) } -} else { - console.warn( - '[Azure Provider] No Azure App Configuration connection string or endpoint found. The configuration adapter will not be available.' - ) } /* We load the infrastructure package dynamically here to avoid including it in the From bb40086207ca8db93069915788784402048ad44c Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Thu, 7 Aug 2025 13:42:15 -0400 Subject: [PATCH 13/23] Adds Azure app config vars to application builder so they're visible to Rockets --- .../src/infrastructure/application-builder.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts index d17f4e774..d09cd330c 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts @@ -28,6 +28,7 @@ export class ApplicationBuilder { webPubSubBaseFile = await FunctionZip.copyBaseZip(this.config) } const azureStack = await this.synthApplication(app, webPubSubBaseFile) + this.populateInfrastructureEnvironmentVariables(azureStack) const rocketBuilder = new RocketBuilder(this.config, azureStack.applicationStack, this.rockets) await rocketBuilder.synthRocket() // add rocket-related env vars to main function app settings @@ -54,6 +55,18 @@ export class ApplicationBuilder { } } + private populateInfrastructureEnvironmentVariables(azureStack: AzureStack): void { + const appConfiguration = azureStack.applicationStack.appConfiguration + if (appConfiguration?.primaryWriteKey) { + this.config.env['AZURE_APP_CONFIG_CONNECTION_STRING'] = `Endpoint=${appConfiguration.name};Id=${ + appConfiguration.primaryWriteKey.get(0).id + };Secret=${appConfiguration.primaryWriteKey.get(0).secret}` + } + if (appConfiguration?.endpoint) { + this.config.env['AZURE_APP_CONFIG_ENDPOINT'] = appConfiguration.endpoint + } + } + private async synthApplication(app: App, destinationFile?: string): Promise { const logger = getLogger(this.config, 'ApplicationBuilder#synthApplication') logger.info('Synth...') From 42e28c678ebb45590f622c0130e8db4a14fb8d99 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Mon, 11 Aug 2025 09:53:23 -0400 Subject: [PATCH 14/23] Fixes label filters and app config connection string in rockets --- .../src/infrastructure/application-builder.ts | 8 +++++--- packages/framework-provider-azure/src/index.ts | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts index d09cd330c..5ee021ac1 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts @@ -58,9 +58,11 @@ export class ApplicationBuilder { private populateInfrastructureEnvironmentVariables(azureStack: AzureStack): void { const appConfiguration = azureStack.applicationStack.appConfiguration if (appConfiguration?.primaryWriteKey) { - this.config.env['AZURE_APP_CONFIG_CONNECTION_STRING'] = `Endpoint=${appConfiguration.name};Id=${ - appConfiguration.primaryWriteKey.get(0).id - };Secret=${appConfiguration.primaryWriteKey.get(0).secret}` + this.config.env['AZURE_APP_CONFIG_CONNECTION_STRING'] = `Endpoint=https://${ + appConfiguration.name + }.azconfig.io;Id=${appConfiguration.primaryWriteKey.get(0).id};Secret=${ + appConfiguration.primaryWriteKey.get(0).secret + }` } if (appConfiguration?.endpoint) { this.config.env['AZURE_APP_CONFIG_ENDPOINT'] = appConfiguration.endpoint diff --git a/packages/framework-provider-azure/src/index.ts b/packages/framework-provider-azure/src/index.ts index 5958201be..827db58de 100644 --- a/packages/framework-provider-azure/src/index.ts +++ b/packages/framework-provider-azure/src/index.ts @@ -115,8 +115,20 @@ if (azureAppConfigConnectionString || azureAppConfigEndpoint) { try { const config = require('@boostercloud/framework-core').Booster.config - const provider = ConfigurationAdapter.fromEnvironment() - config.addConfigurationProvider(provider) + const azureAppConfigOptions = (config as any)._azureAppConfigOptions + + // Use user overrides if provided, otherwise fall back to environment variables + const connectionString = azureAppConfigOptions?.connectionString || azureAppConfigConnectionString + const endpoint = azureAppConfigOptions?.endpoint || azureAppConfigEndpoint + const labelFilter = azureAppConfigOptions?.labelFilter + + // Initialize if we have either environment variables or user config with enabled=true + if (connectionString || endpoint || azureAppConfigOptions?.enabled) { + const provider = connectionString + ? ConfigurationAdapter.withConnectionString(connectionString, labelFilter) + : ConfigurationAdapter.withEndpoint(endpoint, labelFilter) + config.addConfigurationProvider(provider) + } } catch (error) { console.warn('[Azure Provider] Failed to initialize Azure App Configuration adapter:', error) } From ef6db5f4b579c83d9098bc2db98c247128fc52bf Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Tue, 12 Aug 2025 08:06:30 -0400 Subject: [PATCH 15/23] Fixes several typos --- .../src/library/configuration-adapter.ts | 4 +++- packages/framework-types/src/config.ts | 2 +- website/docs/03_features/03_configuration.mdx | 4 ++-- website/docs/10_going-deeper/infrastructure-providers.mdx | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/framework-provider-azure/src/library/configuration-adapter.ts b/packages/framework-provider-azure/src/library/configuration-adapter.ts index 2460691c0..960991df2 100644 --- a/packages/framework-provider-azure/src/library/configuration-adapter.ts +++ b/packages/framework-provider-azure/src/library/configuration-adapter.ts @@ -75,7 +75,9 @@ export class ConfigurationAdapter implements ConfigurationProvider { /** * Create a ConfigurationAdapter instance from environment variables - * this is the standard way to initialize the provider in Azure environments + * This if the standard way to initialize the provider in Azure Function App environments, + * where these environment variables are automatically injected. In other environments, + * you may need to set these variables manually. */ static fromEnvironment(labelFilter?: string): ConfigurationAdapter { const connectionString = process.env['AZURE_APP_CONFIG_CONNECTION_STRING'] diff --git a/packages/framework-types/src/config.ts b/packages/framework-types/src/config.ts index e0dd4a5ff..e0c88ce52 100644 --- a/packages/framework-types/src/config.ts +++ b/packages/framework-types/src/config.ts @@ -45,7 +45,7 @@ export interface ConfigurationProvider { isAvailable(): Promise /** - * Priority of this configuration provider (higher number = higher priority + * Priority of this configuration provider (higher number = higher priority) */ readonly priority: number diff --git a/website/docs/03_features/03_configuration.mdx b/website/docs/03_features/03_configuration.mdx index b47173e90..b08b3464d 100644 --- a/website/docs/03_features/03_configuration.mdx +++ b/website/docs/03_features/03_configuration.mdx @@ -83,7 +83,7 @@ Booster.configure('production', (config: BoosterConfig): void => { You can customize the Azure App Configuration integration: ```typescript title="src/config/config.ts" -Booster.configure('production', (config: BoosterConfig): void => {) +Booster.configure('production', (config: BoosterConfig): void => { config.appName = 'my-app' config.providerPackage = '@boostercloud/framework-provider-azure' @@ -277,7 +277,7 @@ Register your custom provider in the configuration: ```typescript title="src/config/config.ts" import { CustomConfigurationProvider } from '../providers/custom-config-provider' -Booster.configure('production', (config: BoosterConfig): void => {) +Booster.configure('production', (config: BoosterConfig): void => { config.appName = 'my-app' config.providerPackage = '@boostercloud/framework-provider-azure' diff --git a/website/docs/10_going-deeper/infrastructure-providers.mdx b/website/docs/10_going-deeper/infrastructure-providers.mdx index a40ff8ea9..34c60d790 100644 --- a/website/docs/10_going-deeper/infrastructure-providers.mdx +++ b/website/docs/10_going-deeper/infrastructure-providers.mdx @@ -329,7 +329,7 @@ If you want to use your own `host.json` file just add it to `config.assets` arra ### Azure App Configuration Integration -The Azure provider includes seamless integration with [Azure App Configuration service](https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview) for centralized configuration management, When enabled, Booster automatically provisions the necessary infrastructure and provides a unified configuration API. +The Azure provider includes seamless integration with [Azure App Configuration service](https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview) for centralized configuration management. When enabled, Booster automatically provisions the necessary infrastructure and provides a unified configuration API. #### Enabling Azure App Configuration @@ -358,13 +358,13 @@ When you deploy with Azure App Configuration enabled, the Azure provider automat #### Using Configuration Values -Once deployed, you can retrieve configuration avlues from any part of your application: +Once deployed, you can retrieve configuration values from any part of your application: ```typescript import { resolveConfigurationValue, resolveConfigurationWithSource } from '@boostercloud/framework-core' // Simple value resolution -const appTimeout = await resolveConfigurationValue(Booster.config, 'API_TIMEOUT' +const appTimeout = await resolveConfigurationValue(Booster.config, 'API_TIMEOUT') // Resolution with source tracking (useful for debugging) const resolution = await resolveConfigurationWithSource(Booster.config, 'FEATURE_FLAG_X') From 840688d7715fb5a3abc2cbbcea3c1c840103de6d Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Tue, 12 Aug 2025 08:15:59 -0400 Subject: [PATCH 16/23] Fixes more typos --- .../src/library/configuration-adapter.ts | 2 +- .../test/library/configuration-adapter.test.ts | 2 +- packages/framework-types/src/config.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/framework-provider-azure/src/library/configuration-adapter.ts b/packages/framework-provider-azure/src/library/configuration-adapter.ts index 960991df2..1fbf1b7aa 100644 --- a/packages/framework-provider-azure/src/library/configuration-adapter.ts +++ b/packages/framework-provider-azure/src/library/configuration-adapter.ts @@ -75,7 +75,7 @@ export class ConfigurationAdapter implements ConfigurationProvider { /** * Create a ConfigurationAdapter instance from environment variables - * This if the standard way to initialize the provider in Azure Function App environments, + * This is the standard way to initialize the provider in Azure Function App environments, * where these environment variables are automatically injected. In other environments, * you may need to set these variables manually. */ diff --git a/packages/framework-provider-azure/test/library/configuration-adapter.test.ts b/packages/framework-provider-azure/test/library/configuration-adapter.test.ts index 23c323e5a..62be5a691 100644 --- a/packages/framework-provider-azure/test/library/configuration-adapter.test.ts +++ b/packages/framework-provider-azure/test/library/configuration-adapter.test.ts @@ -124,7 +124,7 @@ describe('ConfigurationAdapter', () => { describe('integration scenarios', () => { it('should work with label filters', async () => { - const adapter = new ConfigurationAdapter('mock-connection-string', undefined, 'producton') + const adapter = new ConfigurationAdapter('mock-connection-string', undefined, 'production') // Should create without throwing expect(adapter.name).to.equal('azure-app-configuration') diff --git a/packages/framework-types/src/config.ts b/packages/framework-types/src/config.ts index e0c88ce52..18c9e2f54 100644 --- a/packages/framework-types/src/config.ts +++ b/packages/framework-types/src/config.ts @@ -203,7 +203,7 @@ export class BoosterConfig { /** Environment variables set at deployment time on the target lambda functions */ public readonly env: Record = {} - /** Configuration providers for external configuration sources (Azure App Configuration, etc.) **/ + /** Configuration providers for external configuration sources (Azure App Configuration, etc.) */ public readonly configurationProviders: ConfigurationProvider[] = [] /** From 25dd72196788ea4ea890c328db255e5b02487a27 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Tue, 12 Aug 2025 10:38:37 -0400 Subject: [PATCH 17/23] Adds typing enhancements --- .../synth/terraform-app-configuration.ts | 4 +- .../framework-provider-azure/src/index.ts | 2 +- .../src/library/configuration-adapter.ts | 51 +++--- packages/framework-types/src/config.ts | 151 +++++++++++------- 4 files changed, 112 insertions(+), 96 deletions(-) diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts index 0086ce0d6..66127d99a 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts @@ -13,7 +13,7 @@ export class TerraformAppConfiguration extends Construct { const { appPrefix, resourceGroup } = applicationStack // Check if Azure App Configuration is enabled in the config - const azureAppConfigOptions = (config as any)._azureAppConfigOptions + const azureAppConfigOptions = config.getAzureAppConfigOptions() if (!azureAppConfigOptions?.enabled) { // If not enabled, create a placeholder without actual resources this.appConfiguration = {} as appConfiguration.AppConfiguration @@ -69,7 +69,7 @@ export class TerraformAppConfiguration extends Construct { * Check if App Configuration is enabled for this configuration */ public static isEnabled(config: BoosterConfig): boolean { - const azureAppConfigOptions = (config as any)._azureAppConfigOptions + const azureAppConfigOptions = config.getAzureAppConfigOptions() return azureAppConfigOptions?.enabled === true } } diff --git a/packages/framework-provider-azure/src/index.ts b/packages/framework-provider-azure/src/index.ts index 827db58de..2999c8278 100644 --- a/packages/framework-provider-azure/src/index.ts +++ b/packages/framework-provider-azure/src/index.ts @@ -115,7 +115,7 @@ if (azureAppConfigConnectionString || azureAppConfigEndpoint) { try { const config = require('@boostercloud/framework-core').Booster.config - const azureAppConfigOptions = (config as any)._azureAppConfigOptions + const azureAppConfigOptions = config.getAzureAppConfigOptions() // Use user overrides if provided, otherwise fall back to environment variables const connectionString = azureAppConfigOptions?.connectionString || azureAppConfigConnectionString diff --git a/packages/framework-provider-azure/src/library/configuration-adapter.ts b/packages/framework-provider-azure/src/library/configuration-adapter.ts index 1fbf1b7aa..919e86607 100644 --- a/packages/framework-provider-azure/src/library/configuration-adapter.ts +++ b/packages/framework-provider-azure/src/library/configuration-adapter.ts @@ -18,10 +18,11 @@ export class ConfigurationAdapter implements ConfigurationProvider { /** * Initialize the Azure App Configuration client + * @returns true if initialization was successful, false otherwise */ - private async initialize(): Promise { + private async initialize(): Promise { if (this.isInitialized) { - return + return !this.initializationError } try { @@ -33,23 +34,30 @@ export class ConfigurationAdapter implements ConfigurationProvider { const credential = new DefaultAzureCredential() this.client = new AppConfigurationClient(this.endpoint, credential) } else { - throw new Error('Azure App Configuration requires either a connection string or endpoint URL') + this.initializationError = new Error( + 'Azure App Configuration requires either a connection string or endpoint URL' + ) + this.isInitialized = true + return false } + + this.isInitialized = true + return true } catch (error) { this.initializationError = error instanceof Error ? error : new Error(String(error)) this.isInitialized = true // Mark as initialized to avoid retrying - throw this.initializationError + return false } } async getValue(key: string): Promise { - try { - await this.initialize() + const initialized = await this.initialize() - if (!this.client) { - return undefined - } + if (!initialized || !this.client) { + return undefined + } + try { // Get the configuration setting with optional label filter const configurationSetting = await this.client.getConfigurationSetting({ key, @@ -65,12 +73,8 @@ export class ConfigurationAdapter implements ConfigurationProvider { } async isAvailable(): Promise { - try { - await this.initialize() - return !!this.client && !this.initializationError - } catch (error) { - return false - } + const initialized = await this.initialize() + return initialized && !!this.client && !this.initializationError } /** @@ -100,20 +104,3 @@ export class ConfigurationAdapter implements ConfigurationProvider { return new ConfigurationAdapter(undefined, endpoint, labelFilter) } } - -/** - * Configuration options for Azure App Configuration integration - */ -export interface AzureAppConfigurationOptions { - /** Connection string for Azure App Configuration (alternative to endpoint + managed identity) */ - connectionString?: string - - /** Endpoint URL for Azure App Configuration (used with managed identity) */ - endpoint?: string - - /** Optional label filter to target specific configuration values */ - labelFilter?: string - - /** Whether to enable Azure App Configuration (default: false) */ - enabled?: boolean -} diff --git a/packages/framework-types/src/config.ts b/packages/framework-types/src/config.ts index 18c9e2f54..09754665e 100644 --- a/packages/framework-types/src/config.ts +++ b/packages/framework-types/src/config.ts @@ -27,66 +27,6 @@ import { TraceConfiguration } from './instrumentation/trace-types' import { Context } from 'effect' import { AzureConfiguration, DEFAULT_CHUNK_SIZE } from './provider/azure-configuration' -/** - * Configuration provider interface for external configuration sources - */ -export interface ConfigurationProvider { - /** - * Retrieve a configuration value by key - * @param key The configuration key to retrieve - * @returns Promise resolving to the configuration value or undefined if not found - */ - getValue(key: string): Promise - - /** - * Check if the configuration provider is available and properly initialized - * @returns Promise resolving to a true if available, false otherwise - */ - isAvailable(): Promise - - /** - * Priority of this configuration provider (higher number = higher priority) - */ - readonly priority: number - - /** - * Name identifier for this configuration provider - */ - readonly name: string -} - -/** - * Configuration resolution result with source tracking - */ -export interface ConfigurationResolution { - value: string | undefined - source: string - key: string -} - -/** - * Configuration resolver that manages multiple providers with fallback - */ -export interface ConfigurationResolver { - /** - * Resolve a configuration value from all available providers - * @param key The configuration key to resolve - * @returns Promise resolving to the configuration resolution result - */ - resolve(key: string): Promise - - /** - * Add a configuration provider - * @param provider The configuration provider to add - */ - addProvider(provider: ConfigurationProvider): void - - /** - * Get all registered providers sorted by priority - */ - getProviders(): ConfigurationProvider[] -} - /** * Class used by external packages that needs to get a representation of * the booster config. Used mainly for vendor-specific deployment packages @@ -177,6 +117,17 @@ export class BoosterConfig { // TTL for events stored in dispatched events table. Default to 5 minutes (i.e., 300 seconds). public dispatchedEventsTtl = 300 + /** Azure App Configuration options stored for provider access */ + private _azureAppConfigOptions?: AzureAppConfigurationOptions + + /** + * Get Azure App Configuration options (used by Azure provider package) + * @returns Azure App Configuration options if enabled, undefined otherwise + */ + public getAzureAppConfigOptions(): AzureAppConfigurationOptions | undefined { + return this._azureAppConfigOptions + } + public registerRocketFunction(id: string, func: RocketFunction): void { const currentFunction = this.rocketFunctionMap[id] if (currentFunction) { @@ -293,7 +244,7 @@ export class BoosterConfig { // This method signature needs to remain in framework-types, but the actual implementation // will be provided by the Azure provider package to avoid circular dependencies // Store the options in a special property that the Azure provider can read - ;(this as any)._azureAppConfigOptions = { + this._azureAppConfigOptions = { connectionString: options?.connectionString, endpoint: options?.endpoint, labelFilter: options?.labelFilter, @@ -413,6 +364,84 @@ export interface RetryConfig { retryableErrors?: Array } +/** + * Configuration options for Azure App Configuration integration + * Used to store configuration without circular dependencies + */ +export interface AzureAppConfigurationOptions { + /** Connection string for Azure App Configuration (alternative to endpoint + managed identity) */ + connectionString?: string + + /** Endpoint URL for Azure App Configuration (used with managed identity) */ + endpoint?: string + + /** Optional label filter to target specific configuration values */ + labelFilter?: string + + /** Whether to enable Azure App Configuration (default: false) */ + enabled?: boolean +} + +/** + * Configuration provider interface for external configuration sources + */ +export interface ConfigurationProvider { + /** + * Retrieve a configuration value by key + * @param key The configuration key to retrieve + * @returns Promise resolving to the configuration value or undefined if not found + */ + getValue(key: string): Promise + + /** + * Check if the configuration provider is available and properly initialized + * @returns Promise resolving to a true if available, false otherwise + */ + isAvailable(): Promise + + /** + * Priority of this configuration provider (higher number = higher priority) + */ + readonly priority: number + + /** + * Name identifier for this configuration provider + */ + readonly name: string +} + +/** + * Configuration resolution result with source tracking + */ +export interface ConfigurationResolution { + value: string | undefined + source: string + key: string +} + +/** + * Configuration resolver that manages multiple providers with fallback + */ +export interface ConfigurationResolver { + /** + * Resolve a configuration value from all available providers + * @param key The configuration key to resolve + * @returns Promise resolving to the configuration resolution result + */ + resolve(key: string): Promise + + /** + * Add a configuration provider + * @param provider The configuration provider to add + */ + addProvider(provider: ConfigurationProvider): void + + /** + * Get all registered providers sorted by priority + */ + getProviders(): ConfigurationProvider[] +} + type EntityName = string type EventName = string type CommandName = string From 6d070df4058980a0cf0d179e0c7c6d7258521a33 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Tue, 12 Aug 2025 10:55:29 -0400 Subject: [PATCH 18/23] Adds small changes for type safety --- .../src/services/configuration-service.ts | 11 ++++++++--- .../synth/terraform-function-app-settings.ts | 11 ++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/framework-core/src/services/configuration-service.ts b/packages/framework-core/src/services/configuration-service.ts index f4a512512..8036b4faf 100644 --- a/packages/framework-core/src/services/configuration-service.ts +++ b/packages/framework-core/src/services/configuration-service.ts @@ -1,6 +1,8 @@ import { BoosterConfig, BoosterConfigEnvProvider, + ConfigurationProvider, + ConfigurationResolution, ConfigurationResolver, DefaultConfigurationResolver, EnvironmentVariablesProvider, @@ -55,14 +57,14 @@ export class ConfigurationService { * @param key The configuration key to resolve * @returns Promise resolving to the full configuration resolution result */ - public async resolve(key: string) { + public async resolve(key: string): Promise { return this.resolver.resolve(key) } /** * Get all registered providers */ - public getProviders() { + public getProviders(): ConfigurationProvider[] { return this.resolver.getProviders() } } @@ -79,7 +81,10 @@ export async function resolveConfigurationValue(config: BoosterConfig, key: stri /** * Utility function to resolve a configuration value with source tracking */ -export async function resolveConfigurationWithSource(config: BoosterConfig, key: string) { +export async function resolveConfigurationWithSource( + config: BoosterConfig, + key: string +): Promise { const configService = ConfigurationService.getInstance(config) return configService.resolve(key) } diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts index 9ed3639c3..ea1533365 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts @@ -30,11 +30,12 @@ export class TerraformFunctionAppSettings { const region = (process.env['REGION'] ?? '').toLowerCase().replace(/ /g, '') // Azure App Configuration settings - const appConfigConnectionString = appConfiguration?.primaryWriteKey - ? `Endpoint=https://${appConfiguration.name}.azconfig.io;Id=${ - appConfiguration.primaryWriteKey.get(0).id - };Secret=${appConfiguration.primaryWriteKey.get(0).secret}` - : '' + const appConfigConnectionString = + appConfiguration?.primaryWriteKey && appConfiguration?.name + ? `Endpoint=https://${appConfiguration.name}.azconfig.io;Id=${ + appConfiguration.primaryWriteKey.get(0).id + };Secret=${appConfiguration.primaryWriteKey.get(0).secret}` + : '' const appConfigEndpoint = appConfiguration?.endpoint || '' return { From 8642bdbc8a8c8d5000ac87404ecc65729d569987 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Tue, 12 Aug 2025 12:53:05 -0400 Subject: [PATCH 19/23] Refactored app config connection string creation into a utility method --- .../src/infrastructure/application-builder.ts | 21 +++++++++-------- .../src/infrastructure/helper/utils.ts | 23 +++++++++++++++---- .../synth/terraform-app-configuration.ts | 9 ++++---- .../synth/terraform-function-app-settings.ts | 11 +++++---- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts index 5ee021ac1..420591dbb 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts @@ -2,7 +2,7 @@ import { BoosterConfig } from '@boostercloud/framework-types' import { InfrastructureRocket } from './rockets/infrastructure-rocket' import { AzureStack } from './azure-stack' import * as ckdtfTemplate from './templates/cdktf' -import { renderToFile } from './helper/utils' +import { buildAzureAppConfigConnectionString, renderToFile } from './helper/utils' import { getLogger, Promises } from '@boostercloud/framework-common-helpers' import { App } from 'cdktf' import { ZipResource } from './types/zip-resource' @@ -17,7 +17,8 @@ export interface ApplicationBuild { } export class ApplicationBuilder { - constructor(readonly config: BoosterConfig, readonly rockets?: InfrastructureRocket[]) {} + constructor(readonly config: BoosterConfig, readonly rockets?: InfrastructureRocket[]) { + } public async buildApplication(): Promise { await this.generateSynthFiles() @@ -42,7 +43,7 @@ export class ApplicationBuilder { if (this.config.eventStreamConfiguration.enabled) { consumerZipResource = await FunctionZip.copyZip( azureStack.applicationStack.consumerFunctionDefinitions!, - 'consumerFunctionApp.zip' + 'consumerFunctionApp.zip', ) } const rocketsZipResources = await rocketBuilder.mountRocketsZipResources() @@ -57,12 +58,14 @@ export class ApplicationBuilder { private populateInfrastructureEnvironmentVariables(azureStack: AzureStack): void { const appConfiguration = azureStack.applicationStack.appConfiguration - if (appConfiguration?.primaryWriteKey) { - this.config.env['AZURE_APP_CONFIG_CONNECTION_STRING'] = `Endpoint=https://${ - appConfiguration.name - }.azconfig.io;Id=${appConfiguration.primaryWriteKey.get(0).id};Secret=${ - appConfiguration.primaryWriteKey.get(0).secret - }` + if (appConfiguration?.primaryWriteKey && appConfiguration?.name) { + this.config.env['AZURE_APP_CONFIG_CONNECTION_STRING'] = buildAzureAppConfigConnectionString( + appConfiguration.name, + { + id: appConfiguration.primaryWriteKey.get(0).id, + secret: appConfiguration.primaryWriteKey.get(0).secret, + }, + ) } if (appConfiguration?.endpoint) { this.config.env['AZURE_APP_CONFIG_ENDPOINT'] = appConfiguration.endpoint diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/helper/utils.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/helper/utils.ts index 07edd6f88..544a817ba 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/helper/utils.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/helper/utils.ts @@ -5,7 +5,7 @@ import * as Mustache from 'mustache' import { configuration } from './params' import { WebSiteManagementClient as WebSiteManagement } from '@azure/arm-appservice' import { ResourceManagementClient } from '@azure/arm-resources' -import { TokenCredential, ClientSecretCredential } from '@azure/identity' +import { ClientSecretCredential, TokenCredential } from '@azure/identity' const MAX_TERRAFORM_SIZE_NAME = 24 const MAX_RESOURCE_GROUP_NAME_SIZE = 20 @@ -57,7 +57,7 @@ export function getDeployRegion(): string { const region = process.env['REGION'] if (!region) { throw new Error( - "REGION was not properly loaded and is required to run the deploy process. Check that you've set it in REGION environment variable." + 'REGION was not properly loaded and is required to run the deploy process. Check that you\'ve set it in REGION environment variable.', ) } return region @@ -85,12 +85,12 @@ export async function azureCredentials(): Promise { const applicationTokenCredentials = new ClientSecretCredential( configuration.tenantId, configuration.appId, - configuration.secret + configuration.secret, ) if (!applicationTokenCredentials) { throw new Error( - 'Unable to login with Service Principal. Please verified provided appId, secret and subscription ID in .env file are correct.' + 'Unable to login with Service Principal. Please verified provided appId, secret and subscription ID in .env file are correct.', ) } @@ -113,6 +113,19 @@ export function createDomainNameLabel(resourceGroupName: string): string { return `${resourceGroupName}apis` } +/** + * Builds Azure App Configuration connection string from primary write key details + * @param appConfigName The name of the Azure App Configuration resource + * @param primaryWriteKey The primary write key object containing id and secret + * @returns Formatted connection string for Azure App Configuration + */ +export function buildAzureAppConfigConnectionString( + appConfigName: string, + primaryWriteKey: { id: string; secret: string }, +): string { + return `Endpoint=https://${appConfigName}.azconfig.io;Id=${primaryWriteKey.id};Secret=${primaryWriteKey.secret}` +} + function loadUserProject(userProjectPath: string): UserApp { const projectIndexJSPath = path.resolve(path.join(userProjectPath, 'dist', 'index.js')) return require(projectIndexJSPath) @@ -131,7 +144,7 @@ export async function waitForIt( checkResult: (result: TResult) => boolean, errorMessage: string, timeoutMs = 180000, - tryEveryMs = 1000 + tryEveryMs = 1000, ): Promise { console.debug('[waitForIt] start') const start = Date.now() diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts index 66127d99a..f6c3c76ac 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts @@ -2,7 +2,7 @@ import { Construct } from 'constructs' import { appConfiguration } from '@cdktf/provider-azurerm' import { BoosterConfig } from '@boostercloud/framework-types' import { ApplicationSynthStack } from '../types/application-synth-stack' -import { toTerraformName } from '../helper/utils' +import { buildAzureAppConfigConnectionString, toTerraformName } from '../helper/utils' export class TerraformAppConfiguration extends Construct { public readonly appConfiguration: appConfiguration.AppConfiguration @@ -50,9 +50,10 @@ export class TerraformAppConfiguration extends Construct { if (!this.appConfiguration || !this.appConfiguration.primaryWriteKey) { return '' } - return `Endpoint=https://${this.appConfiguration.name}.azconfig.io;Id=${ - this.appConfiguration.primaryWriteKey.get(0).id - };Secret=${this.appConfiguration.primaryWriteKey.get(0).secret}` + return buildAzureAppConfigConnectionString(this.appConfiguration.name, { + id: this.appConfiguration.primaryWriteKey.get(0).id, + secret: this.appConfiguration.primaryWriteKey.get(0).secret, + }) } /** diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts index ea1533365..bbcc0958e 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts @@ -1,6 +1,6 @@ import { environmentVarNames } from '@boostercloud/framework-provider-azure' import { ApplicationSynthStack } from '../types/application-synth-stack' -import { toTerraformName } from '../helper/utils' +import { buildAzureAppConfigConnectionString, toTerraformName } from '../helper/utils' import { BoosterConfig } from '@boostercloud/framework-types' import { storageAccount } from '@cdktf/provider-azurerm' @@ -17,7 +17,7 @@ export class TerraformFunctionAppSettings { }: ApplicationSynthStack, config: BoosterConfig, storageAccount: storageAccount.StorageAccount, - suffixName: string + suffixName: string, ): { [key: string]: string } { if (!cosmosdbDatabase) { throw new Error('Undefined cosmosdbDatabase resource') @@ -32,9 +32,10 @@ export class TerraformFunctionAppSettings { // Azure App Configuration settings const appConfigConnectionString = appConfiguration?.primaryWriteKey && appConfiguration?.name - ? `Endpoint=https://${appConfiguration.name}.azconfig.io;Id=${ - appConfiguration.primaryWriteKey.get(0).id - };Secret=${appConfiguration.primaryWriteKey.get(0).secret}` + ? buildAzureAppConfigConnectionString(appConfiguration.name, { + id: appConfiguration.primaryWriteKey.get(0).id, + secret: appConfiguration.primaryWriteKey.get(0).secret, + }) : '' const appConfigEndpoint = appConfiguration?.endpoint || '' From c9610f7c2bbce1cae6ecfca113eb402b17b7024b Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Tue, 12 Aug 2025 12:55:40 -0400 Subject: [PATCH 20/23] Adds small documentation change --- .../src/infrastructure/synth/terraform-app-configuration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts index f6c3c76ac..26df798a9 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-app-configuration.ts @@ -26,7 +26,7 @@ export class TerraformAppConfiguration extends Construct { name, resourceGroupName: resourceGroup.name, location: resourceGroup.location, - sku: 'free', // Use free tier by default + sku: 'free', // Use free tier by default. For more information, see https://azure.microsoft.com/en-us/pricing/details/app-configuration/ tags: { Application: config.appName, Environment: config.environmentName, From f8f4d25be8b4953d9b35a11422f1952183bd82fe Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Tue, 12 Aug 2025 18:05:54 -0400 Subject: [PATCH 21/23] Enhances error handling in Azure ConfigurationAdapter initialization --- .../src/library/configuration-adapter.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/framework-provider-azure/src/library/configuration-adapter.ts b/packages/framework-provider-azure/src/library/configuration-adapter.ts index 919e86607..fe2dba36e 100644 --- a/packages/framework-provider-azure/src/library/configuration-adapter.ts +++ b/packages/framework-provider-azure/src/library/configuration-adapter.ts @@ -44,7 +44,13 @@ export class ConfigurationAdapter implements ConfigurationProvider { this.isInitialized = true return true } catch (error) { - this.initializationError = error instanceof Error ? error : new Error(String(error)) + // Preserve original error information by wrapping it + const originalError = error instanceof Error ? error : new Error(String(error)) + this.initializationError = new Error( + `Failed to initialize Azure App Configuration client: ${originalError.message}` + ) + // Preserve the original error as a property for debugging + ;(this.initializationError as any).originalError = originalError this.isInitialized = true // Mark as initialized to avoid retrying return false } From 0a5b36bb0b224b0b69fc5bc3b5520163fe61ca85 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Tue, 12 Aug 2025 18:11:13 -0400 Subject: [PATCH 22/23] Fixes small formatting and linting issues --- .../src/infrastructure/application-builder.ts | 7 +++---- .../src/infrastructure/helper/utils.ts | 10 +++++----- .../synth/terraform-function-app-settings.ts | 6 +++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts index 420591dbb..b2b1add4f 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/application-builder.ts @@ -17,8 +17,7 @@ export interface ApplicationBuild { } export class ApplicationBuilder { - constructor(readonly config: BoosterConfig, readonly rockets?: InfrastructureRocket[]) { - } + constructor(readonly config: BoosterConfig, readonly rockets?: InfrastructureRocket[]) {} public async buildApplication(): Promise { await this.generateSynthFiles() @@ -43,7 +42,7 @@ export class ApplicationBuilder { if (this.config.eventStreamConfiguration.enabled) { consumerZipResource = await FunctionZip.copyZip( azureStack.applicationStack.consumerFunctionDefinitions!, - 'consumerFunctionApp.zip', + 'consumerFunctionApp.zip' ) } const rocketsZipResources = await rocketBuilder.mountRocketsZipResources() @@ -64,7 +63,7 @@ export class ApplicationBuilder { { id: appConfiguration.primaryWriteKey.get(0).id, secret: appConfiguration.primaryWriteKey.get(0).secret, - }, + } ) } if (appConfiguration?.endpoint) { diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/helper/utils.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/helper/utils.ts index 544a817ba..197767f02 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/helper/utils.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/helper/utils.ts @@ -57,7 +57,7 @@ export function getDeployRegion(): string { const region = process.env['REGION'] if (!region) { throw new Error( - 'REGION was not properly loaded and is required to run the deploy process. Check that you\'ve set it in REGION environment variable.', + "REGION was not properly loaded and is required to run the deploy process. Check that you've set it in REGION environment variable." ) } return region @@ -85,12 +85,12 @@ export async function azureCredentials(): Promise { const applicationTokenCredentials = new ClientSecretCredential( configuration.tenantId, configuration.appId, - configuration.secret, + configuration.secret ) if (!applicationTokenCredentials) { throw new Error( - 'Unable to login with Service Principal. Please verified provided appId, secret and subscription ID in .env file are correct.', + 'Unable to login with Service Principal. Please verified provided appId, secret and subscription ID in .env file are correct.' ) } @@ -121,7 +121,7 @@ export function createDomainNameLabel(resourceGroupName: string): string { */ export function buildAzureAppConfigConnectionString( appConfigName: string, - primaryWriteKey: { id: string; secret: string }, + primaryWriteKey: { id: string; secret: string } ): string { return `Endpoint=https://${appConfigName}.azconfig.io;Id=${primaryWriteKey.id};Secret=${primaryWriteKey.secret}` } @@ -144,7 +144,7 @@ export async function waitForIt( checkResult: (result: TResult) => boolean, errorMessage: string, timeoutMs = 180000, - tryEveryMs = 1000, + tryEveryMs = 1000 ): Promise { console.debug('[waitForIt] start') const start = Date.now() diff --git a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts index bbcc0958e..d24d6ece8 100644 --- a/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts +++ b/packages/framework-provider-azure-infrastructure/src/infrastructure/synth/terraform-function-app-settings.ts @@ -17,7 +17,7 @@ export class TerraformFunctionAppSettings { }: ApplicationSynthStack, config: BoosterConfig, storageAccount: storageAccount.StorageAccount, - suffixName: string, + suffixName: string ): { [key: string]: string } { if (!cosmosdbDatabase) { throw new Error('Undefined cosmosdbDatabase resource') @@ -33,8 +33,8 @@ export class TerraformFunctionAppSettings { const appConfigConnectionString = appConfiguration?.primaryWriteKey && appConfiguration?.name ? buildAzureAppConfigConnectionString(appConfiguration.name, { - id: appConfiguration.primaryWriteKey.get(0).id, - secret: appConfiguration.primaryWriteKey.get(0).secret, + id: appConfiguration.primaryWriteKey.get(0).id, + secret: appConfiguration.primaryWriteKey.get(0).secret, }) : '' const appConfigEndpoint = appConfiguration?.endpoint || '' From e01edee57a1203b15ef66c4d62f69eb1138229c1 Mon Sep 17 00:00:00 2001 From: "Castro, Mario" Date: Thu, 21 Aug 2025 18:04:20 -0400 Subject: [PATCH 23/23] Adds small changes to documentation --- website/docs/03_features/03_configuration.mdx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/website/docs/03_features/03_configuration.mdx b/website/docs/03_features/03_configuration.mdx index b08b3464d..ea520be11 100644 --- a/website/docs/03_features/03_configuration.mdx +++ b/website/docs/03_features/03_configuration.mdx @@ -16,11 +16,15 @@ When you request a configuration value, Booster checks each source in order and ```typescript import { Booster } from '@boostercloud/framework-core' -import { resolveConfigurationWithSource } from '@boostercloud/framework-core' +import { resolveConfigurationWithSource, resolveConfigurationValue } from '@boostercloud/framework-core' // Resolve a configuration value with source tracking const resolution = await resolveConfigurationWithSource(Booster.config, 'API_TIMEOUT') console.log(`Value: ${resolution.value}, Source: ${resolution.source}`) + +// Resolve a configuration value without source tracking +const timeout = await resolveConfigurationValue(Booster.config, 'API_TIMEOUT') +console.log(`Timeout: ${timeout}`) ``` ## Built-in Configuration Providers @@ -89,7 +93,7 @@ Booster.configure('production', (config: BoosterConfig): void => { // Advanced configuration config.enableAzureAppConfiguration({ - // Optional: Override connection string (usually set by infrastructure + // Optional: Override connection string (usually set by infrastructure) connectionString: 'Endpoint=https://myappconfig.azconfig.io;Id=xxx;Secret=xxx', // Optional: Override endpoint (alternative to connection string) @@ -155,6 +159,17 @@ In your Azure App Configuration resource, create the same keys with different la | `FEATURE_NEW_CHECKOUT` | `staging` | `enabled` | QA testing | | `FEATURE_NEW_CHECKOUT` | `production` | `disabled` | Stable prod version | +:::important Label Filter Behavior +When you configure a `labelFilter` (e.g., `labelFilter: 'production'`), Azure App Configuration will **only retrieve configuration values that have that specific label**. If you try to access a configuration key that exists in Azure App Configuration but doesn't have the matching label, it will **not be found** and the resolution will fall back to the next provider in the hierarchy (Booster config.env or environment variables). + +For example, if you have: +- A key `API_KEY` with label `development` +- A key `API_KEY` with no label (empty label) +- Your config uses `labelFilter: 'production'` + +Then requesting `API_KEY` will return `undefined` from Azure App Configuration because neither the `development` labeled value nor the unlabeled value matches the `production` filter. The system will then check other configuration sources in the hierarchy. +::: + #### 3. Using Labeled Configuration in Your Code Your application code remains the same - the label filtering is handled automatically: @@ -397,5 +412,3 @@ providers.forEach((provider) => { Booster's configuration management system provides a powerful, flexible way to manage application settings across different environments and sources. By leveraging external providers like Azure App Configuration, you can achieve true configuration-driven applications that adapt without requiring code changes or redeployments. The hierarchical resolution system ensures predictable behavior while providing multiple layers of configuration sources for maximum flexibility and reliability. - -