Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FAI-13685] Write config/catalog files #97

Merged
merged 5 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion airbyte-local-cli-nodejs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ target/
out/pkg
sample_command.sh
*airbyte-local
test/exec/resources/
test/exec/resources
7 changes: 4 additions & 3 deletions airbyte-local-cli-nodejs/src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ function parseSrcAndDstConfig(argv: string[]) {
}

// Convert the options to CliOptions type
function convertToCliOptions(options: any) {
function convertToCliOptions(options: any): CliOptions {
return {
...options,
srcImage: options.src,
Expand All @@ -135,7 +135,7 @@ function convertToCliOptions(options: any) {
}

// Validate the input options
function validateConfigFileInput(config: FarosConfig, inputType: AirbyteConfigInputType) {
function validateConfigFileInput(config: FarosConfig, inputType: AirbyteConfigInputType): void {
if (!config.src?.image && !config.srcInputFile) {
if (inputType === AirbyteConfigInputType.OPTION) {
throw new Error(`Missing source image. Please use '--src <image>' to provide the source image`);
Expand All @@ -156,7 +156,7 @@ function validateConfigFileInput(config: FarosConfig, inputType: AirbyteConfigIn
}

// parse the command line arguments
export function parseAndValidateInputs(argv: string[]) {
export function parseAndValidateInputs(argv: string[]): FarosConfig {
// Parse the command line arguments
const program = command().parse(argv);

Expand Down Expand Up @@ -197,6 +197,7 @@ export function parseAndValidateInputs(argv: string[]) {
rawMessages: cliOptions.rawMessages ?? false,
keepContainers: cliOptions.keepContainers ?? false,
logLevel: cliOptions.logLevel ?? 'info',
debug: cliOptions.debug ?? false,
};

if (cliOptions.srcImage || cliOptions.dstImage) {
Expand Down
5 changes: 3 additions & 2 deletions airbyte-local-cli-nodejs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {parseAndValidateInputs} from './command';
import {AirbyteCliContext} from './types';
import {checkDockerInstalled, cleanUp, createTmpDir, loadStateFile, logger} from './utils';
import {checkDockerInstalled, cleanUp, createTmpDir, loadStateFile, logger, writeConfig} from './utils';

function main() {
function main(): void {
const context: AirbyteCliContext = {};
try {
const cfg = parseAndValidateInputs(process.argv);
checkDockerInstalled();
context.tmpDir = createTmpDir();
loadStateFile(context.tmpDir, cfg?.stateFile, cfg?.connectionName);
writeConfig(context.tmpDir, cfg);
} catch (error: any) {
logger.error(error.message, 'Error');
cleanUp(context);
Expand Down
1 change: 1 addition & 0 deletions airbyte-local-cli-nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@ export interface FarosConfig {
rawMessages: boolean;
keepContainers: boolean;
logLevel: string;
debug: boolean;
}
53 changes: 52 additions & 1 deletion airbyte-local-cli-nodejs/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {sep} from 'node:path';
import pino from 'pino';
import pretty from 'pino-pretty';

import {AirbyteCliContext, AirbyteConfig} from './types';
import {AirbyteCliContext, AirbyteConfig, FarosConfig} from './types';

// constants
export const FILENAME_PREFIX = 'faros_airbyte_cli';

// Create a pino logger instance
export const logger = pino(pretty({colorize: true}));
Expand Down Expand Up @@ -106,3 +109,51 @@ export function cleanUp(context: AirbyteCliContext): void {
}
logger.info('Clean up completed.');
}

// Write Airbyte config and catalog to temporary dir and a json file
export function writeConfig(tmpDir: string, config: FarosConfig): void {
const airbyteConfig = {
src: config.src ?? ({} as AirbyteConfig),
dst: config.dst ?? ({} as AirbyteConfig),
};

// write Airbyte config for user's reference
// TODO: @FAI-14122 React secrets
logger.debug(`Writing Airbyte config for user reference...`);
writeFileSync(`${FILENAME_PREFIX}_config.json`, JSON.stringify(airbyteConfig, null, 2));
logger.debug(airbyteConfig, `Airbyte config: `);
logger.debug(`Airbyte config written to: ${FILENAME_PREFIX}_config.json`);

// add config `feed_cfg.debug` if debug is enabled
const regex = /^farosai\/airbyte-faros-feeds-source.*/;
if (config.debug && regex.exec(airbyteConfig.src.image ?? '')) {
airbyteConfig.src.config = {
...airbyteConfig.src.config,
feed_cfg: {debug: true},
};
}

// write config to temporary directory config files
logger.debug(`Writing Airbyte config to files...`);
const srcConfigFilePath = `${tmpDir}${sep}${FILENAME_PREFIX}_src_config.json`;
const dstConfigFilePath = `${tmpDir}${sep}${FILENAME_PREFIX}_dst_config.json`;
writeFileSync(srcConfigFilePath, JSON.stringify(airbyteConfig.src.config ?? {}, null, 2));
writeFileSync(dstConfigFilePath, JSON.stringify(airbyteConfig.dst.config ?? {}, null, 2));
logger.debug(`Airbyte config files written to: ${srcConfigFilePath}, ${dstConfigFilePath}`);

// write catalog to temporary directory catalog files
// TODO: @FAI-14134 Discover catalog
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discover catalog will be covered in another ticket as we need to spin up docker

logger.debug(`Writing Airbyte catalog to files...`);
const srcCatalogFilePath = `${tmpDir}${sep}${FILENAME_PREFIX}_src_catalog.json`;
const dstCatalogFilePath = `${tmpDir}${sep}${FILENAME_PREFIX}_dst_catalog.json`;
if (
(!airbyteConfig.dst.catalog || Object.keys(airbyteConfig.dst.catalog).length === 0) &&
airbyteConfig.src.catalog &&
Object.keys(airbyteConfig.src.catalog).length > 0
) {
airbyteConfig.dst.catalog = airbyteConfig.src.catalog;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ok for now, but be prepared for logic generate a destination-compatible catalog based on the src catalog. It has slightly different format.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohhhh i see. thanks for the heads up

}
writeFileSync(srcCatalogFilePath, JSON.stringify(airbyteConfig.src.catalog ?? {}, null, 2));
writeFileSync(dstCatalogFilePath, JSON.stringify(airbyteConfig.dst.catalog ?? {}, null, 2));
logger.debug(`Airbyte catalog files written to: ${srcCatalogFilePath}, ${dstCatalogFilePath}`);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`loadStateFile should pass with existing state file 1`] = `
"{"format":"base64/gzip","data":"dGVzdA=="}
"
`;

exports[`parseConfigFile should pass 1`] = `
{
"dst": {
Expand Down Expand Up @@ -32,3 +27,8 @@ exports[`parseConfigFile should pass 1`] = `
},
}
`;

exports[`write files to temporary dir loadStateFile should pass with existing state file 1`] = `
"{"format":"base64/gzip","data":"dGVzdA=="}
"
`;
3 changes: 3 additions & 0 deletions airbyte-local-cli-nodejs/test/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const defaultConfig = {
rawMessages: false,
keepContainers: false,
logLevel: 'info',
debug: false,
};

afterEach(() => {
Expand Down Expand Up @@ -196,6 +197,7 @@ describe('Check other options', () => {
rawMessages: true,
keepContainers: true,
logLevel: 'debug',
debug: true,
});
});

Expand All @@ -207,6 +209,7 @@ describe('Check other options', () => {
src: {image: 'source-image', config: {}},
dst: {image: 'destination-image', config: {}},
logLevel: 'debug',
debug: true,
});
});

Expand Down

This file was deleted.

This file was deleted.

140 changes: 123 additions & 17 deletions airbyte-local-cli-nodejs/test/utils.it.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import {existsSync, mkdtempSync, readFileSync, rmSync} from 'node:fs';
import {tmpdir} from 'node:os';

import {checkDockerInstalled, cleanUp, createTmpDir, loadStateFile, parseConfigFile} from '../src/utils';
import {FarosConfig} from '../src/types';
import {
checkDockerInstalled,
cleanUp,
createTmpDir,
FILENAME_PREFIX,
loadStateFile,
parseConfigFile,
writeConfig,
} from '../src/utils';

describe('parseConfigFile', () => {
it('should pass', () => {
Expand Down Expand Up @@ -35,31 +45,127 @@ describe('createTmpDir', () => {
});
});

describe('loadStateFile', () => {
describe('write files to temporary dir', () => {
let tmpDirPath: string;
beforeAll(() => {
tmpDirPath = mkdtempSync('test-temp-dir');
tmpDirPath = mkdtempSync(`${tmpdir()}/test-temp-dir`);
});
afterAll(() => {
rmSync(tmpDirPath, {recursive: true, force: true});
});

it('should pass without existing state file', () => {
expect(() => loadStateFile(tmpDirPath)).not.toThrow();
expect(existsSync(`${tmpDirPath}/state.json`)).toBe(true);
expect(readFileSync(`${tmpDirPath}/state.json`, 'utf8')).toBe('{}');
});
describe('loadStateFile', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't change these tests. just wrap it in another describe

it('should pass without existing state file', () => {
expect(() => loadStateFile(tmpDirPath)).not.toThrow();
expect(existsSync(`${tmpDirPath}/state.json`)).toBe(true);
expect(readFileSync(`${tmpDirPath}/state.json`, 'utf8')).toBe('{}');
});

it('should pass with existing state file', () => {
const testStateFile = 'test/resources/test__state.json';
expect(() => loadStateFile(tmpDirPath, testStateFile)).not.toThrow();
expect(existsSync(`${tmpDirPath}/state.json`)).toBe(true);
expect(readFileSync(`${tmpDirPath}/state.json`, 'utf8')).toMatchSnapshot();
});

it('should pass with existing state file', () => {
const testStateFile = 'test/resources/test__state.json';
expect(() => loadStateFile(tmpDirPath, testStateFile)).not.toThrow();
expect(existsSync(`${tmpDirPath}/state.json`)).toBe(true);
expect(readFileSync(`${tmpDirPath}/state.json`, 'utf8')).toMatchSnapshot();
it('should fail if state file is not loaded', () => {
expect(() => loadStateFile(tmpDirPath, 'non-exist-state-file')).toThrow(
`State file 'non-exist-state-file' not found. Please make sure the state file exists and have read access.`,
);
});
});

it('should fail if state file is not loaded', () => {
expect(() => loadStateFile(tmpDirPath, 'non-exist-state-file')).toThrow(
`State file 'non-exist-state-file' not found. Please make sure the state file exists and have read access.`,
);
describe('writeConfig', () => {
const testConfig: FarosConfig = {
src: {
image: 'farosai/airbyte-test-source',
config: {
username: 'test',
password: 'test',
url: 'test',
},
catalog: {
tests: {disabled: true},
projects: {disabled: true},
},
},
dst: {
image: 'farosai/airbyte-test-destination',
config: {
edition_config: {
graph: 'default',
edition: 'cloud',
api_url: 'https://test.api.faros.ai',
},
},
},

// default values
srcCheckConnection: false,
dstUseHostNetwork: false,
srcPull: false,
dstPull: false,
fullRefresh: false,
rawMessages: false,
keepContainers: false,
logLevel: 'info',
debug: false,
stateFile: undefined,
connectionName: undefined,
srcOutputFile: undefined,
srcInputFile: undefined,
};

afterEach(() => {
rmSync(`${FILENAME_PREFIX}_config.json`, {force: true});
rmSync(`${tmpDirPath}/${FILENAME_PREFIX}_src_config.json`, {force: true});
rmSync(`${tmpDirPath}/${FILENAME_PREFIX}_dst_config.json`, {force: true});
rmSync(`${tmpDirPath}/${FILENAME_PREFIX}_src_catalog.json`, {force: true});
rmSync(`${tmpDirPath}/${FILENAME_PREFIX}_dst_catalog.json`, {force: true});
});

it('should write files', () => {
expect(() => writeConfig(tmpDirPath, structuredClone(testConfig))).not.toThrow();
expect(existsSync(`${FILENAME_PREFIX}_config.json`)).toBe(true);
expect(existsSync(`${tmpDirPath}/${FILENAME_PREFIX}_src_config.json`)).toBe(true);
expect(existsSync(`${tmpDirPath}/${FILENAME_PREFIX}_dst_config.json`)).toBe(true);
expect(existsSync(`${tmpDirPath}/${FILENAME_PREFIX}_src_catalog.json`)).toBe(true);
expect(existsSync(`${tmpDirPath}/${FILENAME_PREFIX}_dst_catalog.json`)).toBe(true);

expect(readFileSync(`${FILENAME_PREFIX}_config.json`, 'utf8')).toEqual(
JSON.stringify({src: testConfig.src, dst: testConfig.dst}, null, 2),
);
expect(readFileSync(`${tmpDirPath}/${FILENAME_PREFIX}_src_config.json`, 'utf8')).toEqual(
JSON.stringify(testConfig.src?.config, null, 2),
);
expect(readFileSync(`${tmpDirPath}/${FILENAME_PREFIX}_dst_config.json`, 'utf8')).toEqual(
JSON.stringify(testConfig.dst?.config, null, 2),
);
expect(readFileSync(`${tmpDirPath}/${FILENAME_PREFIX}_src_catalog.json`, 'utf8')).toEqual(
JSON.stringify(testConfig.src?.catalog, null, 2),
);
expect(readFileSync(`${tmpDirPath}/${FILENAME_PREFIX}_dst_catalog.json`, 'utf8')).toEqual(
JSON.stringify(testConfig.src?.catalog, null, 2),
);
});

it('should alter config if debug is enabled', () => {
const testConfigDebug = {...structuredClone(testConfig), debug: true};
(testConfigDebug.src as any).image = 'farosai/airbyte-faros-feeds-source:v1';
expect(() => writeConfig(tmpDirPath, structuredClone(testConfigDebug))).not.toThrow();
expect(existsSync(`${FILENAME_PREFIX}_config.json`)).toBe(true);
expect(existsSync(`${tmpDirPath}/${FILENAME_PREFIX}_src_config.json`)).toBe(true);
expect(existsSync(`${tmpDirPath}/${FILENAME_PREFIX}_dst_config.json`)).toBe(true);

expect(readFileSync(`${FILENAME_PREFIX}_config.json`, 'utf8')).toEqual(
JSON.stringify({src: testConfigDebug.src, dst: testConfigDebug.dst}, null, 2),
);
expect(readFileSync(`${tmpDirPath}/${FILENAME_PREFIX}_src_config.json`, 'utf8')).toEqual(
JSON.stringify({...testConfigDebug.src?.config, feed_cfg: {debug: true}}, null, 2),
);
expect(readFileSync(`${tmpDirPath}/${FILENAME_PREFIX}_dst_config.json`, 'utf8')).toEqual(
JSON.stringify(testConfigDebug.dst?.config, null, 2),
);
});
});
});
Loading