diff --git a/lib/Config.js b/lib/Config.js index 3dea761b30..717f139837 100644 --- a/lib/Config.js +++ b/lib/Config.js @@ -1810,6 +1810,14 @@ class Config extends EventEmitter { if (config.enableVeeamRoute !== undefined && config.enableVeeamRoute !== null) { this.enableVeeamRoute = config.enableVeeamRoute; } + + if (config.capabilities) { + if (config.capabilities.locationTypes) { + config.capabilities.locationTypes = new Set(config.capabilities.locationTypes); + } + this.capabilities = config.capabilities; + } + return config; } diff --git a/lib/utilities/reportHandler.js b/lib/utilities/reportHandler.js index 1316662f13..6b42ee5fe2 100644 --- a/lib/utilities/reportHandler.js +++ b/lib/utilities/reportHandler.js @@ -34,25 +34,50 @@ function hasWSOptionalDependencies() { } } -function getCapabilities() { - const localVolumeCap = process.env.LOCAL_VOLUME_CAPABILITY || 'true'; - return { +function getCapabilities(cfg = config) { + const caps = cfg.capabilities || { + // Default capabilities, for backward compatibility. Should not be modified, + // changes are expected to be done through config.json file. + locationTypeAzure: true, + locationTypeGCP: true, locationTypeDigitalOcean: true, locationTypeS3Custom: true, locationTypeSproxyd: true, locationTypeNFS: true, locationTypeCephRadosGW: true, locationTypeHyperdriveV2: true, - locationTypeLocal: localVolumeCap === '1' || localVolumeCap.toLowerCase() === 'true', + locationTypeLocal: true, preferredReadLocation: true, managedLifecycle: true, managedLifecycleTransition: true, - secureChannelOptimizedPath: hasWSOptionalDependencies(), + secureChannelOptimizedPath: true, s3cIngestLocation: true, nfsIngestLocation: false, cephIngestLocation: false, awsIngestLocation: false, }; + + // Consistency & safety checks for capabilities that depend on other config values + const localVolumeCap = process.env.LOCAL_VOLUME_CAPABILITY || 'true'; + caps.locationTypeLocal &&= (localVolumeCap === '1' || localVolumeCap.toLowerCase() === 'true'); + caps.secureChannelOptimizedPath &&= hasWSOptionalDependencies(); + caps.managedLifecycle &&= cfg.supportedLifecycleRules.includes('Expiration'); + caps.managedLifecycleTransition &&= cfg.supportedLifecycleRules.includes('Transition'); + caps.lifecycleRules &&= cfg.supportedLifecycleRules; + + // Map locationTypes entries to the respective "legacy" capability flags + if (caps.locationTypes) { + caps.locationTypeAzure &&= caps.locationTypes.includes('location-azure-v1'); + caps.locationTypeGCP &&= caps.locationTypes.includes('location-gcp-v1'); + caps.locationTypeDigitalOcean &&= caps.locationTypes.includes('location-do-spaces-v1'); + caps.locationTypeSproxyd &&= caps.locationTypes.includes('location-scality-sproxyd-v1'); + caps.locationTypeNFS &&= caps.locationTypes.includes('location-nfs-mount-v1'); + caps.locationTypeCephRadosGW &&= caps.locationTypes.includes('location-ceph-radosgw-s3-v1'); + caps.locationTypeHyperdriveV2 &&= caps.locationTypes.includes('location-scality-hdclient-v2'); + caps.locationTypeLocal &&= caps.locationTypes.includes('location-file-v1'); + } + + return caps; } function cleanup(obj) { diff --git a/tests/unit/utils/reportHandler.js b/tests/unit/utils/reportHandler.js new file mode 100644 index 0000000000..8189ecc55f --- /dev/null +++ b/tests/unit/utils/reportHandler.js @@ -0,0 +1,290 @@ +const assert = require('assert'); +const sinon = require('sinon'); +const { getCapabilities } = require('../../../lib/utilities/reportHandler'); + +describe('reportHandler.getCapabilities', () => { + const sandbox = sinon.createSandbox(); + const originalEnv = process.env.LOCAL_VOLUME_CAPABILITY; + + afterEach(() => { + sandbox.restore(); + if (originalEnv === undefined) { + delete process.env.LOCAL_VOLUME_CAPABILITY; + } else { + process.env.LOCAL_VOLUME_CAPABILITY = originalEnv; + } + }); + + describe('getCapabilities', () => { + it('should return default capabilities when config.capabilities is not set', () => { + const cfg = { + supportedLifecycleRules: ['Expiration', 'Transition', 'NoncurrentVersionExpiration'], + }; + const caps = getCapabilities(cfg); + + assert.deepStrictEqual(caps, { + locationTypeAzure: true, + locationTypeGCP: true, + locationTypeDigitalOcean: true, + locationTypeS3Custom: true, + locationTypeSproxyd: true, + locationTypeNFS: true, + locationTypeCephRadosGW: true, + locationTypeHyperdriveV2: true, + locationTypeLocal: true, + preferredReadLocation: true, + managedLifecycle: true, + managedLifecycleTransition: true, + secureChannelOptimizedPath: true, + s3cIngestLocation: true, + nfsIngestLocation: false, + cephIngestLocation: false, + awsIngestLocation: false, + }); + }); + + it('should use capabilities from config when specified', () => { + const cfg = { + capabilities: { + locationTypeAzure: false, + locationTypeGCP: false, + locationTypeDigitalOcean: true, + locationTypeS3Custom: false, + customCapability: 'test-value', + }, + supportedLifecycleRules: ['Expiration'], + }; + const caps = getCapabilities(cfg); + + assert.deepStrictEqual(caps, { + locationTypeAzure: false, + locationTypeGCP: false, + locationTypeDigitalOcean: true, + locationTypeS3Custom: false, + customCapability: 'test-value', + }); + }); + + it('should apply LOCAL_VOLUME_CAPABILITY env when set to false', () => { + process.env.LOCAL_VOLUME_CAPABILITY = 'false'; + const cfg = { + capabilities: { + locationTypeLocal: true, + }, + supportedLifecycleRules: ['Expiration'], + }; + const caps = getCapabilities(cfg); + + // locationTypeLocal should be false due to env variable + assert.strictEqual(caps.locationTypeLocal, false); + }); + + it('should apply LOCAL_VOLUME_CAPABILITY env when set to "0"', () => { + process.env.LOCAL_VOLUME_CAPABILITY = '0'; + const cfg = { + capabilities: { + locationTypeLocal: true, + }, + supportedLifecycleRules: ['Expiration'], + }; + const caps = getCapabilities(cfg); + + // locationTypeLocal should be false due to env variable + assert.strictEqual(caps.locationTypeLocal, false); + }); + + it('should apply LOCAL_VOLUME_CAPABILITY env when set to true', () => { + process.env.LOCAL_VOLUME_CAPABILITY = '1'; + const cfg = { + capabilities: { + locationTypeLocal: true, + }, + supportedLifecycleRules: ['Expiration'], + }; + const caps = getCapabilities(cfg); + + // locationTypeLocal should remain true + assert.strictEqual(caps.locationTypeLocal, true); + }); + + it('should not apply LOCAL_VOLUME_CAPABILITY env if locationTypeLocal disabled', () => { + process.env.LOCAL_VOLUME_CAPABILITY = true; + const cfg = { + capabilities: { + locationTypeLocal: false, + }, + supportedLifecycleRules: ['Expiration'], + }; + const caps = getCapabilities(cfg); + + // locationTypeLocal should remain true + assert.strictEqual(caps.locationTypeLocal, false); + }); + + it('should disable managedLifecycle when Expiration is not in supportedLifecycleRules', () => { + const cfg = { + capabilities: { + managedLifecycle: true, + }, + supportedLifecycleRules: ['Transition', 'NoncurrentVersionExpiration'], + }; + const caps = getCapabilities(cfg); + + // managedLifecycle should be false + assert.strictEqual(caps.managedLifecycle, false); + }); + + it('should keep managedLifecycle when Expiration is in supportedLifecycleRules', () => { + const cfg = { + capabilities: { + managedLifecycle: true, + }, + supportedLifecycleRules: ['Expiration', 'Transition'], + }; + const caps = getCapabilities(cfg); + + // managedLifecycle should remain true + assert.strictEqual(caps.managedLifecycle, true); + }); + + it('should not enable managedLifecycle if managedLifecycle is disabled', () => { + const cfg = { + capabilities: { + managedLifecycle: false, + }, + supportedLifecycleRules: ['Expiration', 'Transition'], + }; + const caps = getCapabilities(cfg); + + // managedLifecycle should remain true + assert.strictEqual(caps.managedLifecycle, false); + }); + + it('should disable managedLifecycleTransition when Transition is not in supportedLifecycleRules', () => { + const cfg = { + capabilities: { + managedLifecycleTransition: true, + }, + supportedLifecycleRules: ['Expiration', 'NoncurrentVersionExpiration'], + }; + const caps = getCapabilities(cfg); + + // managedLifecycleTransition should be false + assert.strictEqual(caps.managedLifecycleTransition, false); + }); + + it('should keep managedLifecycleTransition when Transition is in supportedLifecycleRules', () => { + const cfg = { + capabilities: { + managedLifecycleTransition: true, + }, + supportedLifecycleRules: ['Expiration', 'Transition'], + }; + const caps = getCapabilities(cfg); + + // managedLifecycleTransition should remain true + assert.strictEqual(caps.managedLifecycleTransition, true); + }); + + it('should not enable managedLifecycleTransition if managedLifecycleTransition is disabled', () => { + const cfg = { + capabilities: { + managedLifecycleTransition: true, + }, + supportedLifecycleRules: ['Expiration', 'Transition'], + }; + const caps = getCapabilities(cfg); + + // managedLifecycleTransition should remain true + assert.strictEqual(caps.managedLifecycleTransition, true); + }); + + it('should override lifecycleRules from supportedLifecycleRules', () => { + const supportedRules = ['Expiration', 'Transition', 'NoncurrentVersionExpiration']; + const cfg = { + capabilities: { + lifecycleRules: ['Expiration'], + }, + supportedLifecycleRules: supportedRules, + }; + const caps = getCapabilities(cfg); + + // lifecycleRules should be set to supportedLifecycleRules + assert.deepStrictEqual(caps.lifecycleRules, supportedRules); + }); + + it('should update locationTypes capabilities based on locationTypes map', () => { + const cfg = { + capabilities: { + locationTypeAzure: true, + locationTypeGCP: true, + locationTypeDigitalOcean: true, + locationTypeS3Custom: true, + locationTypeSproxyd: true, + locationTypeNFS: true, + locationTypeCephRadosGW: true, + locationTypeHyperdriveV2: true, + locationTypeLocal: true, + locationTypes: [ + 'location-gcp-v1', + 'location-scality-sproxyd-v1', + 'location-ceph-radosgw-s3-v1', + 'location-file-v1', + 'location-scality-artesca-s3-v1', + ], + }, + supportedLifecycleRules: ['Expiration'], + }; + const caps = getCapabilities(cfg); + + // Verify locationTypes override the legacy flags + assert.strictEqual(caps.locationTypeAzure, false); + assert.strictEqual(caps.locationTypeGCP, true); + assert.strictEqual(caps.locationTypeDigitalOcean, false); + assert.strictEqual(caps.locationTypeSproxyd, true); + assert.strictEqual(caps.locationTypeNFS, false); + assert.strictEqual(caps.locationTypeCephRadosGW, true); + assert.strictEqual(caps.locationTypeHyperdriveV2, false); + assert.strictEqual(caps.locationTypeLocal, true); + }); + + it('should handle multiple consistency checks together', () => { + process.env.LOCAL_VOLUME_CAPABILITY = '0'; + const cfg = { + capabilities: { + locationTypeLocal: true, + secureChannelOptimizedPath: true, + managedLifecycle: true, + managedLifecycleTransition: true, + locationTypeAzure: true, + locationTypeGCP: true, + locationTypes: ['location-azure-v1'], + }, + supportedLifecycleRules: ['Expiration'], // Missing Transition + }; + const caps = getCapabilities(cfg); + + // All consistency checks should be applied + assert.strictEqual(caps.locationTypeLocal, false); // env override + assert.strictEqual(caps.managedLifecycle, true); // Expiration present + assert.strictEqual(caps.managedLifecycleTransition, false); // Transition missing + assert.strictEqual(caps.locationTypeAzure, true); // locationTypes override + assert.strictEqual(caps.locationTypeGCP, false); // locationTypes override + }); + + it('should not modify capabilities when locationTypes is not defined', () => { + const cfg = { + capabilities: { + locationTypeAzure: true, + locationTypeGCP: false, + }, + supportedLifecycleRules: ['Expiration'], + }; + const caps = getCapabilities(cfg); + + // Original values should be preserved + assert.strictEqual(caps.locationTypeAzure, true); + assert.strictEqual(caps.locationTypeGCP, false); + }); + }); +});