diff --git a/tools/integration/src/__tests__/handlers/init.test.ts b/tools/integration/src/__tests__/handlers/init.test.ts index 448895d..251021e 100644 --- a/tools/integration/src/__tests__/handlers/init.test.ts +++ b/tools/integration/src/__tests__/handlers/init.test.ts @@ -316,5 +316,67 @@ describe('Init Handler', () => { const packageJson = JSON.parse(writeCall[1]); expect(packageJson.publishConfig).toEqual({ access: 'public' }); }); + + it('should call generateCollectorFiles when collector is selected', async () => { + (input as jest.Mock) + .mockResolvedValueOnce('test-package') + .mockResolvedValueOnce('1.0.0') + .mockResolvedValueOnce('Description') + .mockResolvedValueOnce('') + .mockResolvedValueOnce('Author') + .mockResolvedValueOnce('MIT'); + + (checkbox as jest.Mock).mockResolvedValue(['dashboards', 'collector']); + (utils.generateReadme as jest.Mock).mockImplementation(() => {}); + (utils.generateCollectorFiles as jest.Mock).mockImplementation(() => {}); + (utils.printDirectoryTree as jest.Mock).mockImplementation(() => {}); + + await handleInit(); + + expect(utils.generateCollectorFiles).toHaveBeenCalledWith( + '/test/dir/test-package', + 'test-package', + ['dashboards', 'collector'] + ); + }); + + it('should not call generateCollectorFiles when collector is not selected', async () => { + (input as jest.Mock) + .mockResolvedValueOnce('test-package') + .mockResolvedValueOnce('1.0.0') + .mockResolvedValueOnce('Description') + .mockResolvedValueOnce('') + .mockResolvedValueOnce('Author') + .mockResolvedValueOnce('MIT'); + + (checkbox as jest.Mock).mockResolvedValue(['dashboards', 'events']); + (utils.generateReadme as jest.Mock).mockImplementation(() => {}); + (utils.generateCollectorFiles as jest.Mock).mockImplementation(() => {}); + (utils.printDirectoryTree as jest.Mock).mockImplementation(() => {}); + + await handleInit(); + + expect(utils.generateCollectorFiles).not.toHaveBeenCalled(); + }); + + it('should create collector folder when collector is selected', async () => { + (input as jest.Mock) + .mockResolvedValueOnce('test-package') + .mockResolvedValueOnce('1.0.0') + .mockResolvedValueOnce('Description') + .mockResolvedValueOnce('') + .mockResolvedValueOnce('Author') + .mockResolvedValueOnce('MIT'); + + (checkbox as jest.Mock).mockResolvedValue(['collector']); + (utils.generateReadme as jest.Mock).mockImplementation(() => {}); + (utils.generateCollectorFiles as jest.Mock).mockImplementation(() => {}); + (utils.printDirectoryTree as jest.Mock).mockImplementation(() => {}); + + await handleInit(); + + expect(fs.mkdirSync).toHaveBeenCalledWith('/test/dir/test-package/collector', { recursive: true }); + expect(logger.info).toHaveBeenCalledWith('Created the integration package sub-folder: /test/dir/test-package/collector'); + }); }); }); \ No newline at end of file diff --git a/tools/integration/src/__tests__/handlers/lint.test.ts b/tools/integration/src/__tests__/handlers/lint.test.ts index 5552559..0ab6747 100644 --- a/tools/integration/src/__tests__/handlers/lint.test.ts +++ b/tools/integration/src/__tests__/handlers/lint.test.ts @@ -352,6 +352,7 @@ describe('Lint Handler', () => { (validators.validateDashboardFiles as jest.Mock).mockImplementation(() => {}); (validators.validateEventFiles as jest.Mock).mockImplementation(() => {}); (validators.validateSmartAlertFiles as jest.Mock).mockImplementation(() => {}); + (validators.validateCollectorFiles as jest.Mock).mockImplementation(() => {}); (fs.existsSync as jest.Mock).mockReturnValue(true); await expect(handleLint(argv)).rejects.toThrow('process.exit(0)'); @@ -360,6 +361,49 @@ describe('Lint Handler', () => { expect(validators.validateDashboardFiles).toHaveBeenCalled(); expect(validators.validateEventFiles).toHaveBeenCalled(); expect(validators.validateSmartAlertFiles).toHaveBeenCalled(); + expect(validators.validateCollectorFiles).toHaveBeenCalled(); + }); + + it('should validate collector when collector folder exists', async () => { + const argv = { debug: false, 'strict-mode': false }; + const packageData = { name: 'test-package', private: false }; + const readmeContent = '# Test'; + + (utils.readPackageJson as jest.Mock).mockReturnValue(packageData); + (utils.isPrivatePackage as jest.Mock).mockReturnValue(false); + (utils.readReadmeFile as jest.Mock).mockReturnValue(readmeContent); + (validators.validatePackageJson as jest.Mock).mockResolvedValue(undefined); + (validators.validateReadmeContent as jest.Mock).mockImplementation(() => {}); + (validators.validateCollectorFiles as jest.Mock).mockImplementation(() => {}); + (fs.existsSync as jest.Mock).mockImplementation((path: string) => { + return path.includes('collector'); + }); + + await expect(handleLint(argv)).rejects.toThrow('process.exit(0)'); + + expect(validators.validateCollectorFiles).toHaveBeenCalledWith( + '/test/package/collector', + expect.any(Array), + expect.any(Array), + expect.any(Array) + ); + }); + + it('should log info when no collector folder found', async () => { + const argv = { debug: false, 'strict-mode': false }; + const packageData = { name: 'test-package', private: false }; + const readmeContent = '# Test'; + + (utils.readPackageJson as jest.Mock).mockReturnValue(packageData); + (utils.isPrivatePackage as jest.Mock).mockReturnValue(false); + (utils.readReadmeFile as jest.Mock).mockReturnValue(readmeContent); + (validators.validatePackageJson as jest.Mock).mockResolvedValue(undefined); + (validators.validateReadmeContent as jest.Mock).mockImplementation(() => {}); + (fs.existsSync as jest.Mock).mockReturnValue(false); + + await expect(handleLint(argv)).rejects.toThrow('process.exit(0)'); + + expect(logger.info).toHaveBeenCalledWith('No collector folder found for this package.'); }); }); }); \ No newline at end of file diff --git a/tools/integration/src/__tests__/utils.test.ts b/tools/integration/src/__tests__/utils.test.ts index f219849..db18d98 100644 --- a/tools/integration/src/__tests__/utils.test.ts +++ b/tools/integration/src/__tests__/utils.test.ts @@ -3,6 +3,7 @@ import * as utils from '../utils'; import { beforeEach, describe, expect, it, jest } from '@jest/globals'; import fs from 'fs'; +import logger from '../logger'; import path from 'path'; // Mock dependencies @@ -573,5 +574,78 @@ describe('Utils Module', () => { const result = utils.filterElementsBy(objects, ['unknown=value']); expect(result).toHaveLength(0); }); + + describe('generateCollectorFiles', () => { + it('should create all 4 collector files with content', () => { + const packagePath = '/test/package'; + const packageName = '@instana-integration/test'; + const configTypes = ['collector']; + + utils.generateCollectorFiles(packagePath, packageName, configTypes); + + // Verify all 4 files were created with some content + expect(mockedFs.writeFileSync).toHaveBeenCalledTimes(4); + + const calls = (mockedFs.writeFileSync as jest.Mock).mock.calls; + calls.forEach((call: any) => { + expect(call[1]).toBeTruthy(); // Has content + expect(call[1].length).toBeGreaterThan(0); // Content is not empty + }); + }); + + it('should create Dockerfile', () => { + utils.generateCollectorFiles('/test/package', '@instana-integration/test', ['collector']); + + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('Dockerfile'), + expect.any(String) + ); + }); + + it('should create Python collector file with normalized name', () => { + utils.generateCollectorFiles('/test/package', '@instana-integration/my-test', ['collector']); + + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('my-test_collector.py'), + expect.any(String) + ); + }); + + it('should create requirements.txt', () => { + utils.generateCollectorFiles('/test/package', '@instana-integration/test', ['collector']); + + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('requirements.txt'), + expect.any(String) + ); + }); + + it('should create config.json', () => { + utils.generateCollectorFiles('/test/package', '@instana-integration/test', ['collector']); + + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('config.json'), + expect.any(String) + ); + }); + + it('should normalize package name by replacing slashes with underscores', () => { + utils.generateCollectorFiles('/test/package', '@instana-integration/sub/package/name', ['collector']); + + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('sub_package_name_collector.py'), + expect.any(String) + ); + }); + + it('should handle package names without @instana-integration prefix', () => { + utils.generateCollectorFiles('/test/package', 'custom-package', ['collector']); + + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('custom-package_collector.py'), + expect.any(String) + ); + }); + }); }); }); \ No newline at end of file diff --git a/tools/integration/src/__tests__/validators.test.ts b/tools/integration/src/__tests__/validators.test.ts index de31871..b08c75d 100644 --- a/tools/integration/src/__tests__/validators.test.ts +++ b/tools/integration/src/__tests__/validators.test.ts @@ -1047,9 +1047,128 @@ describe('validators', () => { expect(errors).toHaveLength(0); }); }); - }); - - describe('validateServerAddress', () => { + + describe('validateCollectorFiles', () => { + it('should validate all required files are present', () => { + const collectorPath = '/test/collector'; + mockedFs.readdirSync.mockReturnValue(['Dockerfile', 'requirements.txt', 'config.json', 'test_collector.py'] as any); + mockedFs.statSync.mockReturnValue({ size: 100 } as any); + + validators.validateCollectorFiles(collectorPath, errors, warnings, successMessages); + + expect(errors).toHaveLength(0); + expect(warnings).toHaveLength(0); + }); + + it('should report error when Dockerfile is missing', () => { + const collectorPath = '/test/collector'; + mockedFs.readdirSync.mockReturnValue(['requirements.txt', 'config.json', 'test_collector.py'] as any); + mockedFs.statSync.mockReturnValue({ size: 100 } as any); + + validators.validateCollectorFiles(collectorPath, errors, warnings, successMessages); + + expect(errors).toContain('Missing required collector file: Dockerfile'); + }); + + it('should report error when requirements.txt is missing', () => { + const collectorPath = '/test/collector'; + mockedFs.readdirSync.mockReturnValue(['Dockerfile', 'config.json', 'test_collector.py'] as any); + mockedFs.statSync.mockReturnValue({ size: 100 } as any); + + validators.validateCollectorFiles(collectorPath, errors, warnings, successMessages); + + expect(errors).toContain('Missing required collector file: requirements.txt'); + }); + + it('should report error when config.json is missing', () => { + const collectorPath = '/test/collector'; + mockedFs.readdirSync.mockReturnValue(['Dockerfile', 'requirements.txt', 'test_collector.py'] as any); + mockedFs.statSync.mockReturnValue({ size: 100 } as any); + + validators.validateCollectorFiles(collectorPath, errors, warnings, successMessages); + + expect(errors).toContain('Missing required collector file: config.json'); + }); + + it('should warn when Python collector file is missing', () => { + const collectorPath = '/test/collector'; + mockedFs.readdirSync.mockReturnValue(['Dockerfile', 'requirements.txt', 'config.json'] as any); + mockedFs.statSync.mockReturnValue({ size: 100 } as any); + + validators.validateCollectorFiles(collectorPath, errors, warnings, successMessages); + + expect(warnings).toContain('Missing Python collector file (.py)'); + }); + + it('should warn when files are empty', () => { + const collectorPath = '/test/collector'; + mockedFs.readdirSync.mockReturnValue(['Dockerfile', 'requirements.txt', 'config.json', 'test_collector.py'] as any); + mockedFs.statSync.mockReturnValue({ size: 0 } as any); + + validators.validateCollectorFiles(collectorPath, errors, warnings, successMessages); + + expect(warnings).toContain('Collector file is empty: Dockerfile'); + expect(warnings).toContain('Collector file is empty: requirements.txt'); + expect(warnings).toContain('Collector file is empty: config.json'); + expect(warnings).toContain('Python collector file is empty: test_collector.py'); + }); + + it('should report error when collector directory is empty', () => { + const collectorPath = '/test/collector'; + mockedFs.readdirSync.mockReturnValue([] as any); + + validators.validateCollectorFiles(collectorPath, errors, warnings, successMessages); + + expect(errors).toContain('No files found in the collector folder.'); + }); + + it('should handle errors gracefully', () => { + const collectorPath = '/test/collector'; + mockedFs.readdirSync.mockImplementation(() => { + throw new Error('Permission denied'); + }); + + validators.validateCollectorFiles(collectorPath, errors, warnings, successMessages); + + expect(errors).toContain('Error validating collector files: Permission denied'); + }); + + it('should accept Python collector files with different names', () => { + const collectorPath = '/test/collector'; + mockedFs.readdirSync.mockReturnValue([ + 'Dockerfile', + 'requirements.txt', + 'config.json', + 'my_custom_collector.py' + ] as any); + mockedFs.statSync.mockReturnValue({ size: 100 } as any); + + validators.validateCollectorFiles(collectorPath, errors, warnings, successMessages); + + expect(errors).toHaveLength(0); + expect(warnings).toHaveLength(0); + }); + + it('should validate mixed file sizes correctly', () => { + const collectorPath = '/test/collector'; + mockedFs.readdirSync.mockReturnValue(['Dockerfile', 'requirements.txt', 'config.json', 'test_collector.py'] as any); + + // Mock different file sizes + let callCount = 0; + mockedFs.statSync.mockImplementation(() => { + callCount++; + return { size: callCount === 2 ? 0 : 100 } as any; // Second file (requirements.txt) is empty + }); + + validators.validateCollectorFiles(collectorPath, errors, warnings, successMessages); + + expect(warnings).toContain('Collector file is empty: requirements.txt'); + expect(warnings).toHaveLength(1); + }); + }); +}); + +describe('validateServerAddress', () => { it('should accept valid server addresses without protocol', () => { expect(() => validateServerAddress('example.com')).not.toThrow(); expect(() => validateServerAddress('api.example.com')).not.toThrow(); @@ -1061,31 +1180,31 @@ describe('validators', () => { it('should reject server addresses with http:// protocol', () => { expect(() => validateServerAddress('http://example.com')).toThrow( - 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "http://example.com"' + 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname.' ); }); it('should reject server addresses with https:// protocol', () => { expect(() => validateServerAddress('https://example.com')).toThrow( - 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "https://example.com"' + 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname.' ); }); it('should reject server addresses with https:// protocol and port', () => { expect(() => validateServerAddress('https://example.com:8080')).toThrow( - 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "https://example.com:8080"' + 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname.' ); }); it('should reject server addresses with other protocols', () => { expect(() => validateServerAddress('ftp://example.com')).toThrow( - 'Invalid server address: Protocol prefix detected. Please use only the hostname, e.g., "example.com" instead of "ftp://example.com"' + 'Invalid server address: Protocol prefix detected. Please use only the hostname.' ); }); it('should handle server addresses with whitespace', () => { expect(() => validateServerAddress(' https://example.com ')).toThrow( - 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "https://example.com"' + 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname.' ); }); @@ -1120,8 +1239,7 @@ describe('validators', () => { { type: 'dashboard', conditions: [], explicitlyTyped: true }, { type: 'event', conditions: [], explicitlyTyped: true }, { type: 'entity', conditions: [], explicitlyTyped: true }, - { type: 'smart-alert', conditions: [], explicitlyTyped: true }, - { type: 'all', conditions: [], explicitlyTyped: true } + { type: 'smart-alert', conditions: [], explicitlyTyped: true } ]; expect(() => validateIncludeTypes(validIncludes)).not.toThrow(); }); @@ -1139,7 +1257,7 @@ describe('validators', () => { { type: 'dashboards', conditions: [], explicitlyTyped: true } ]; expect(() => validateIncludeTypes(invalidIncludes)).toThrow( - 'Invalid --include type value(s): "dashboards". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"' + 'Invalid --include type value(s): "dashboards". Valid types are: "dashboard", "event", "entity", "smart-alert"' ); }); @@ -1148,7 +1266,7 @@ describe('validators', () => { { type: 'events', conditions: [], explicitlyTyped: true } ]; expect(() => validateIncludeTypes(invalidIncludes)).toThrow( - 'Invalid --include type value(s): "events". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"' + 'Invalid --include type value(s): "events". Valid types are: "dashboard", "event", "entity", "smart-alert"' ); }); @@ -1157,7 +1275,7 @@ describe('validators', () => { { type: 'entities', conditions: [], explicitlyTyped: true } ]; expect(() => validateIncludeTypes(invalidIncludes)).toThrow( - 'Invalid --include type value(s): "entities". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"' + 'Invalid --include type value(s): "entities". Valid types are: "dashboard", "event", "entity", "smart-alert"' ); }); @@ -1166,7 +1284,7 @@ describe('validators', () => { { type: 'invalid-type', conditions: [], explicitlyTyped: true } ]; expect(() => validateIncludeTypes(invalidIncludes)).toThrow( - 'Invalid --include type value(s): "invalid-type". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"' + 'Invalid --include type value(s): "invalid-type". Valid types are: "dashboard", "event", "entity", "smart-alert"' ); }); @@ -1177,7 +1295,7 @@ describe('validators', () => { { type: 'dashboards', conditions: [], explicitlyTyped: true } // duplicate ]; expect(() => validateIncludeTypes(invalidIncludes)).toThrow( - 'Invalid --include type value(s): "dashboards", "events". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"' + 'Invalid --include type value(s): "dashboards", "events". Valid types are: "dashboard", "event", "entity", "smart-alert"' ); }); @@ -1201,6 +1319,6 @@ describe('validators', () => { }); it('should have correct valid types constant', () => { - expect(VALID_INCLUDE_TYPES).toEqual(['dashboard', 'event', 'entity', 'smart-alert', 'all']); + expect(VALID_INCLUDE_TYPES).toEqual(['dashboard', 'event', 'entity', 'smart-alert']); }); - }); \ No newline at end of file +}); \ No newline at end of file diff --git a/tools/integration/src/handlers/init.ts b/tools/integration/src/handlers/init.ts index 2b58d0b..2b1be95 100644 --- a/tools/integration/src/handlers/init.ts +++ b/tools/integration/src/handlers/init.ts @@ -49,8 +49,7 @@ export async function handleInit(): Promise { { name: 'events', value: 'events'}, { name: 'entities', value: 'entities'}, { name: 'smart alerts', value: 'smart-alerts'}, - new Separator('-- Below items are not supported yet --'), - { name: 'collector configs', value: 'collector-configs', disabled: true, }, + { name: 'collector', value: 'collector'}, ], required: true }); @@ -67,6 +66,10 @@ export async function handleInit(): Promise { logger.info(`Created the integration package sub-folder: ${configTypePath}`); }); + if (configTypes.includes('collector')) { + utils.generateCollectorFiles(packagePath, packageName, configTypes); + } + const packageJson: { name: string; version: string; diff --git a/tools/integration/src/handlers/lint.ts b/tools/integration/src/handlers/lint.ts index 3af97f8..8e0aea0 100644 --- a/tools/integration/src/handlers/lint.ts +++ b/tools/integration/src/handlers/lint.ts @@ -27,6 +27,7 @@ export async function handleLint(argv: any): Promise { const eventsPath = path.join(currentDirectory, 'events'); const entitiesPath = path.join(currentDirectory, 'entities'); const smartAlertPath = path.join(currentDirectory, 'smart-alerts'); + const collectorPath = path.join(currentDirectory, 'collector'); let embeddedDashboardRefs = new Set(); @@ -70,6 +71,12 @@ export async function handleLint(argv: any): Promise { } else { logger.info('No smart alerts folder found for this package.'); } + + if (fs.existsSync(collectorPath)) { + validators.validateCollectorFiles(collectorPath, errors, warnings, successMessages); + } else { + logger.info('No collector folder found for this package.'); + } } catch (error) { errors.push(`Linting failed: ${error}`); } diff --git a/tools/integration/src/templates/collector/Dockerfile b/tools/integration/src/templates/collector/Dockerfile new file mode 100644 index 0000000..80d4186 --- /dev/null +++ b/tools/integration/src/templates/collector/Dockerfile @@ -0,0 +1,24 @@ +# Multi-stage build for Python collector +FROM python:3.9-slim as builder + +WORKDIR /app + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir --user -r requirements.txt + +# Final stage +FROM python:3.9-slim + +WORKDIR /app + +# Copy installed dependencies from builder +COPY --from=builder /root/.local /root/.local +COPY {{COLLECTOR_NAME}}_collector.py . +COPY config.json . + +# Make sure scripts in .local are usable +ENV PATH=/root/.local/bin:$PATH + +# Run the collector +CMD ["python", "{{COLLECTOR_NAME}}_collector.py"] diff --git a/tools/integration/src/templates/collector/collector.py b/tools/integration/src/templates/collector/collector.py new file mode 100644 index 0000000..eb189f1 --- /dev/null +++ b/tools/integration/src/templates/collector/collector.py @@ -0,0 +1 @@ +Collector file content diff --git a/tools/integration/src/templates/collector/config.json b/tools/integration/src/templates/collector/config.json new file mode 100644 index 0000000..73f5519 --- /dev/null +++ b/tools/integration/src/templates/collector/config.json @@ -0,0 +1,22 @@ +# Replace with actual config + +{ + "extension_id": "sap-monitor", + "extension_name": "sap-monitor", + "extension_version": "1.0.0", + "image": { + "registry": "quay.io", + "repository": "instana-collectors/sap", + "tag": "1.0.0" + }, + "configuration": { + "interval": 60, + "timeout": 30, + "batch_size": 100, + "log_level": "INFO" + }, + "metadata": { + "created_at": "2026-03-22T00:00:00Z", + "created_by": "stanctl-integration" + } +} \ No newline at end of file diff --git a/tools/integration/src/templates/collector/requirements.txt b/tools/integration/src/templates/collector/requirements.txt new file mode 100644 index 0000000..c3a2576 --- /dev/null +++ b/tools/integration/src/templates/collector/requirements.txt @@ -0,0 +1 @@ +Requirements content \ No newline at end of file diff --git a/tools/integration/src/utils.ts b/tools/integration/src/utils.ts index 7271452..59a4343 100644 --- a/tools/integration/src/utils.ts +++ b/tools/integration/src/utils.ts @@ -265,6 +265,16 @@ export function generateReadme(packagePath: string, packageName: string, configT (Note: Write your package description here.) `; + + if (configTypes.includes('collector')) { + readmeContent += ` +## Collector + +Below are the collectors that are currently supported by this integration package. +`; + + } + if (configTypes.includes('dashboards')) { readmeContent += ` ## Dashboards @@ -398,4 +408,37 @@ $ stanctl-integration import --package ${packageName} \\ const readmeFilePath = path.join(packagePath, 'README.md'); fs.writeFileSync(readmeFilePath, readmeContent); logger.info(`Created the package README file at ${readmeFilePath}`); +} + +/** + * Generate collector template files +*/ +export function generateCollectorFiles(packagePath: string, packageName: string, configTypes: string[]) { + const normalizedPackageName = packageName + .replace(/^@instana-integration\//, '') + .split('/') + .filter(Boolean) + .join('_'); + + const targetDir = path.join(packagePath, 'collector'); + const templatesDir = __dirname.includes('/dist') + ? path.join(__dirname, '..', 'src', 'templates', 'collector') + : path.join(__dirname, 'templates', 'collector'); + + // Dockerfile template + let dockerfileContent = fs.readFileSync(path.join(templatesDir, 'Dockerfile'), 'utf-8'); + dockerfileContent = dockerfileContent.replace(/\{\{COLLECTOR_NAME\}\}/g, normalizedPackageName); + fs.writeFileSync(path.join(targetDir, 'Dockerfile'), dockerfileContent); + + // collector file template + const collectorContent = fs.readFileSync(path.join(templatesDir, 'collector.py'), 'utf-8'); + fs.writeFileSync(path.join(targetDir, `${normalizedPackageName}_collector.py`), collectorContent); + + // requirements.txt template + const requirementsContent = fs.readFileSync(path.join(templatesDir, 'requirements.txt'), 'utf-8'); + fs.writeFileSync(path.join(targetDir, 'requirements.txt'), requirementsContent); + + // config.json template + const configContent = fs.readFileSync(path.join(templatesDir, 'config.json'), 'utf-8'); + fs.writeFileSync(path.join(targetDir, 'config.json'), configContent); } \ No newline at end of file diff --git a/tools/integration/src/validators.ts b/tools/integration/src/validators.ts index c5b1c7a..b5eec3d 100644 --- a/tools/integration/src/validators.ts +++ b/tools/integration/src/validators.ts @@ -489,4 +489,46 @@ export function validateSmartAlertFiles( errors.push(`Error validating file ${filePath}: ${error instanceof Error ? error.message : String(error)}.`); } }); +} + +export function validateCollectorFiles(collectorPath: string, errors: string[], warnings: string[], successMessages: string[]): void { + const requiredFiles = ['Dockerfile', 'requirements.txt', 'config.json']; + + try { + const files = fs.readdirSync(collectorPath); + + if (files.length === 0) { + errors.push('No files found in the collector folder.'); + return; + } + + // Check for required files + requiredFiles.forEach(requiredFile => { + if (!files.includes(requiredFile)) { + errors.push(`Missing required collector file: ${requiredFile}`); + } else { + const filePath = path.join(collectorPath, requiredFile); + const stats = fs.statSync(filePath); + if (stats.size === 0) { + warnings.push(`Collector file is empty: ${requiredFile}`); + } + } + }); + + // Check for Python collector file + const pythonCollectorFiles = files.filter(file => file.endsWith('.py')); + if (pythonCollectorFiles.length === 0) { + warnings.push('Missing Python collector file (.py)'); + } else { + const collectorFile = pythonCollectorFiles[0]; + const filePath = path.join(collectorPath, collectorFile); + const stats = fs.statSync(filePath); + if (stats.size === 0) { + warnings.push(`Python collector file is empty: ${collectorFile}`); + } + } + + } catch (error) { + errors.push(`Error validating collector files: ${error instanceof Error ? error.message : String(error)}`); + } } \ No newline at end of file