diff --git a/packages/cli/src/modules/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts b/packages/cli/src/modules/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts index 9cc1ee1e8d9..7f590f70c67 100644 --- a/packages/cli/src/modules/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts +++ b/packages/cli/src/modules/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts @@ -210,6 +210,57 @@ describe('External Secrets Manager', () => { expect(updateSecretsSpy).toHaveBeenCalledTimes(1); }); + + test('should remove all providers when DB returns null', async () => { + await manager.init(); + + // Verify we have providers initially + expect(manager.getProviderNames()).toEqual(['dummy']); + + const disconnectSpy = jest.spyOn(manager.getProvider('dummy')!, 'disconnect'); + + // Mock DB to return null (settings were deleted) + settingsRepo.findByKey.mockResolvedValueOnce(null); + + await manager.reloadAllProviders(); + + // Providers should be disconnected and removed + expect(disconnectSpy).toHaveBeenCalledTimes(1); + expect(manager.getProviderNames()).toEqual([]); + expect(manager.hasProvider('dummy')).toBe(false); + }); + + test('should remove providers no longer in settings', async () => { + // Initialize with multiple providers + mockProvidersInstance.setProviders({ + dummy: DummyProvider, + another_dummy: AnotherDummyProvider, + }); + + await manager.init(); + + // Verify both providers exist + expect(manager.getProviderNames()).toContain('dummy'); + expect(manager.getProviderNames()).toContain('another_dummy'); + + const disconnectSpy = jest.spyOn(manager.getProvider('another_dummy')!, 'disconnect'); + + // Update settings to only include 'dummy' + const updatedSettings = { + dummy: providerSettings(), + }; + settingsRepo.findByKey.mockResolvedValueOnce( + mock({ value: JSON.stringify(updatedSettings) }), + ); + + await manager.reloadAllProviders(); + + // 'another_dummy' should be disconnected and removed + expect(disconnectSpy).toHaveBeenCalledTimes(1); + expect(manager.hasProvider('dummy')).toBe(true); + expect(manager.hasProvider('another_dummy')).toBe(false); + expect(manager.getProviderNames()).toEqual(['dummy']); + }); }); describe('getProviderWithSettings', () => { diff --git a/packages/cli/src/modules/external-secrets.ee/external-secrets-manager.ee.ts b/packages/cli/src/modules/external-secrets.ee/external-secrets-manager.ee.ts index 15177c8bc2b..2c1668fc3eb 100644 --- a/packages/cli/src/modules/external-secrets.ee/external-secrets-manager.ee.ts +++ b/packages/cli/src/modules/external-secrets.ee/external-secrets-manager.ee.ts @@ -47,25 +47,23 @@ export class ExternalSecretsManager implements IExternalSecretsManager { async init(): Promise { if (this.initialized) return; - if (!this.initializingPromise) { - this.initializingPromise = (async () => { - try { - await this.internalInit(); - this.updateInterval = setInterval( - async () => await this.updateSecrets(), - this.config.updateInterval * 1000, - ); - this.initialized = true; - } catch (error) { - this.logger.error('External secrets manager failed to initialize', { - error: ensureError(error), - }); - throw error; - } finally { - this.initializingPromise = undefined; - } - })(); - } + this.initializingPromise ??= (async () => { + try { + await this.internalInit(); + this.updateInterval = setInterval( + async () => await this.updateSecrets(), + this.config.updateInterval * 1000, + ); + this.initialized = true; + } catch (error) { + this.logger.error('External secrets manager failed to initialize', { + error: ensureError(error), + }); + throw error; + } finally { + this.initializingPromise = undefined; + } + })(); await this.initializingPromise; this.logger.debug('External secrets manager initialized'); @@ -86,11 +84,28 @@ export class ExternalSecretsManager implements IExternalSecretsManager { @OnPubSubEvent('reload-external-secrets-providers') async reloadAllProviders(backoff?: number) { this.logger.debug('Reloading all external secrets providers'); - const providers = this.getProviderNames(); - if (!providers) { - return; + + // Refresh settings from DB to get latest changes from other instances + this.cachedSettings = (await this.getDecryptedSettings()) ?? {}; + + const newProviders = new Set(Object.keys(this.cachedSettings)); + const existingProviders = Object.keys(this.providers); + + // Disconnect and remove providers that are no longer in settings + for (const provider of existingProviders) { + if (!newProviders.has(provider)) { + this.logger.debug(`Removing provider ${provider} - no longer in settings`); + try { + await this.providers[provider].disconnect(); + } catch { + this.logger.warn(`Error disconnecting provider ${provider} during removal`); + } + delete this.providers[provider]; + } } - for (const provider of providers) { + + // Reload providers that are in the new settings + for (const provider of newProviders) { await this.reloadProvider(provider, backoff); } @@ -166,7 +181,11 @@ export class ExternalSecretsManager implements IExternalSecretsManager { } catch (e) { try { await provider.disconnect(); - } catch {} + } catch { + this.logger.warn( + `Error disconnecting provider ${provider.displayName} (${provider.name}) after failed connect attempt.`, + ); + } this.logger.error( `Error initializing secrets provider ${provider.displayName} (${provider.name}).`, ); @@ -266,7 +285,11 @@ export class ExternalSecretsManager implements IExternalSecretsManager { async reloadProvider(provider: string, backoff = EXTERNAL_SECRETS_INITIAL_BACKOFF) { if (provider in this.providers) { - await this.providers[provider].disconnect(); + try { + await this.providers[provider].disconnect(); + } catch { + this.logger.warn(`Error disconnecting provider ${provider} during reload`); + } delete this.providers[provider]; } const newProvider = await this.initProvider(provider, this.cachedSettings[provider], backoff);