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

Deployer logs file #1611

Merged
merged 15 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from 10 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
5 changes: 5 additions & 0 deletions .changeset/pink-beds-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@api3/airnode-deployer': patch
---

Write deployer logs to file
3 changes: 3 additions & 0 deletions packages/airnode-deployer/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ coverage/
.terraform*
terraform.tfstate*
*.tfvars

# Logs
/logs
9 changes: 7 additions & 2 deletions packages/airnode-deployer/src/cli/commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import fs from 'fs';
import { join } from 'path';
import { mockReadFileSync } from '../../test/mock-utils';
import { readFileSync } from 'fs';
import { receipt } from '@api3/airnode-validator';
import { deploy, removeWithReceipt } from './commands';
import { version as packageVersion } from '../../package.json';
import * as logger from '../utils/logger';
import { removeAirnode } from '../infrastructure';

const readExampleConfig = () => JSON.parse(readFileSync(join(__dirname, '../../config/config.example.json'), 'utf-8'));
const readExampleConfig = () =>
JSON.parse(fs.readFileSync(join(__dirname, '../../config/config.example.json'), 'utf-8'));

jest.mock('../infrastructure', () => ({
...jest.requireActual('../infrastructure'),
Expand All @@ -20,6 +21,10 @@ jest.mock('../utils', () => ({
writeReceiptFile: jest.fn(),
}));

jest.spyOn(fs, 'appendFileSync').mockImplementation(() => jest.fn());
jest.spyOn(fs, 'mkdirSync').mockImplementation();
logger.setLogsDirectory('/config/logs/');

const gcpReceipt: receipt.Receipt = {
airnodeWallet: {
airnodeAddress: '0xF347ADEd76F7AC2013e379078738aBfF75780C2e',
Expand Down
42 changes: 22 additions & 20 deletions packages/airnode-deployer/src/cli/commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { loadConfig } from '@api3/airnode-node';
import { go } from '@api3/promise-utils';
import { bold } from 'chalk';
import { deployAirnode, removeAirnode } from '../infrastructure';
import { writeReceiptFile, parseReceiptFile, parseSecretsFile } from '../utils';
import * as logger from '../utils/logger';
Expand All @@ -25,36 +24,33 @@ export async function deploy(configPath: string, secretsPath: string, receiptFil

if (!goDeployAirnode.success && !autoRemove) {
logger.fail(
bold(
`Airnode deployment failed due to unexpected errors.\n` +
` It is possible that some resources have been deployed on cloud provider.\n` +
` Please use the "remove" command from the deployer CLI to ensure all cloud resources are removed.`
)
`Airnode deployment failed due to unexpected errors.\n` +
` It is possible that some resources have been deployed on cloud provider.\n` +
` Please use the "remove" command from the deployer CLI to ensure all cloud resources are removed.`,
{ bold: true }
);

throw goDeployAirnode.error;
}

if (!goDeployAirnode.success) {
logger.fail(
bold(
`Airnode deployment failed due to unexpected errors.\n` +
` It is possible that some resources have been deployed on cloud provider.\n` +
` Attempting to remove them...\n`
)
`Airnode deployment failed due to unexpected errors.\n` +
` It is possible that some resources have been deployed on cloud provider.\n` +
` Attempting to remove them...\n`,
{ bold: true }
);

// Try to remove deployed resources
const goRemoveAirnode = await go(() => removeWithReceipt(receiptFile));
if (!goRemoveAirnode.success) {
logger.fail(
bold(
`Airnode removal failed due to unexpected errors.\n` +
` It is possible that some resources have been deployed on cloud provider.\n` +
` Please check the resources on the cloud provider dashboard and\n` +
` use the "remove" command from the deployer CLI to remove them.\n` +
` If the automatic removal via CLI fails, remove the resources manually.`
)
`Airnode removal failed due to unexpected errors.\n` +
` It is possible that some resources have been deployed on cloud provider.\n` +
` Please check the resources on the cloud provider dashboard and\n` +
` use the "remove" command from the deployer CLI to remove them.\n` +
` If the automatic removal via CLI fails, remove the resources manually.`,
{ bold: true }
);

throw new MultiMessageError([
Expand All @@ -68,8 +64,14 @@ export async function deploy(configPath: string, secretsPath: string, receiptFil
}

const output = goDeployAirnode.data;
if (output.httpGatewayUrl) logger.info(`HTTP gateway URL: ${output.httpGatewayUrl}`);
if (output.httpSignedDataGatewayUrl) logger.info(`HTTP signed data gateway URL: ${output.httpSignedDataGatewayUrl}`);
if (output.httpGatewayUrl) {
logger.setSecret(output.httpGatewayUrl);
logger.info(`HTTP gateway URL: ${output.httpGatewayUrl}`, { secrets: true });
}
if (output.httpSignedDataGatewayUrl) {
logger.setSecret(output.httpSignedDataGatewayUrl);
logger.info(`HTTP signed data gateway URL: ${output.httpSignedDataGatewayUrl}`, { secrets: true });
}
}

export async function removeWithReceipt(receiptFilename: string) {
Expand Down
16 changes: 14 additions & 2 deletions packages/airnode-deployer/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function drawHeader() {
async function runCommand(command: () => Promise<void>) {
const goCommand = await go(command);
if (!goCommand.success) {
loggerUtils.log('\n\n\nError details:');
logger.consoleLog('\n\n\nError details:');

// Logging an error here likely results in excessive logging since the errors are usually logged at the place where they
// happen. However if we do not log the error here we risk having unhandled silent errors. The risk is not worth it.
Expand All @@ -46,7 +46,7 @@ async function runCommand(command: () => Promise<void>) {
}

const cliExamples = [
'deploy -c config/config.json -s config/secrets.env -r config/receipt.json',
'deploy -c config/config.json -s config/secrets.env -r config/receipt.json -l config/logs/',
'list --cloud-providers gcp',
'info aws808e2a22',
'fetch-files aws808e2a22',
Expand All @@ -60,6 +60,12 @@ yargs(hideBin(process.argv))
default: false,
type: 'boolean',
})
.option('logs', {
alias: 'l',
description: 'Output path for log files',
default: 'config/logs/',
type: 'string',
})
.command(
'deploy',
'Executes Airnode deployments specified in the config file',
Expand Down Expand Up @@ -93,6 +99,7 @@ yargs(hideBin(process.argv))
drawHeader();

logger.debugMode(args.debug as boolean);
logger.setLogsDirectory(args.logs as string);
logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`);
await runCommand(() => deploy(args.configuration, args.secrets, args.receipt, args['auto-remove']));
}
Expand All @@ -112,6 +119,7 @@ yargs(hideBin(process.argv))
drawHeader();

logger.debugMode(args.debug as boolean);
logger.setLogsDirectory(args.logs as string);
logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`);

await runCommand(() => removeWithReceipt(args.receipt));
Expand All @@ -131,6 +139,7 @@ yargs(hideBin(process.argv))
drawHeader();

logger.debugMode(args.debug as boolean);
logger.setLogsDirectory(args.logs as string);
logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`);

await runCommand(() =>
Expand All @@ -155,6 +164,7 @@ yargs(hideBin(process.argv))
},
async (args) => {
logger.debugMode(args.debug as boolean);
logger.setLogsDirectory(args.logs as string);
logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`);

await listAirnodes(args.cloudProviders);
Expand All @@ -171,6 +181,7 @@ yargs(hideBin(process.argv))
},
async (args) => {
logger.debugMode(args.debug as boolean);
logger.setLogsDirectory(args.logs as string);
logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`);

// Looks like due to the bug in yargs (https://github.com/yargs/yargs/issues/1649) we need to specify the type explicitely
Expand Down Expand Up @@ -203,6 +214,7 @@ yargs(hideBin(process.argv))
},
async (args) => {
logger.debugMode(args.debug as boolean);
logger.setLogsDirectory(args.logs as string);
logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`);

// Looks like due to the bug in yargs (https://github.com/yargs/yargs/issues/1649) we need to specify the types explicitely
Expand Down
5 changes: 5 additions & 0 deletions packages/airnode-deployer/src/infrastructure/aws.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from './aws';
import { mockBucketDirectoryStructure, mockBucketDirectoryStructureList } from '../../test/fixtures';
import { Directory } from '../utils/infrastructure';
import { setLogsDirectory } from '../utils/logger';

const mockPromise = (fn: Function) => () => ({ promise: fn });

Expand Down Expand Up @@ -53,6 +54,10 @@ const awsDeleteObjectsSpy: jest.SpyInstance = jest.requireMock('aws-sdk').S3().d
const awsDeleteBucketSpy: jest.SpyInstance = jest.requireMock('aws-sdk').S3().deleteBucket;
const generateBucketNameSpy: jest.SpyInstance = jest.requireMock('../utils/infrastructure').generateBucketName;

jest.spyOn(fs, 'appendFileSync').mockImplementation(() => jest.fn());
jest.spyOn(fs, 'mkdirSync').mockImplementation();
setLogsDirectory('/config/logs/');

const cloudProvider = {
type: 'aws' as const,
region: 'us-east-1',
Expand Down
6 changes: 6 additions & 0 deletions packages/airnode-deployer/src/infrastructure/gcp.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from 'fs';
import {
copyFileInBucket,
createAirnodeBucket,
Expand All @@ -10,6 +11,7 @@ import {
} from './gcp';
import { mockBucketDirectoryStructure, mockBucketDirectoryStructureList } from '../../test/fixtures';
import { Directory } from '../utils/infrastructure';
import { setLogsDirectory } from '../utils/logger';

const bucketName = 'airnode-aabbccdd0011';

Expand Down Expand Up @@ -63,6 +65,10 @@ const gcsCopySpy: jest.SpyInstance = jest.requireMock('@google-cloud/storage').S
const gcsFileDeleteSpy: jest.SpyInstance = jest.requireMock('@google-cloud/storage').Storage().bucket().file().delete;
const generateBucketNameSpy: jest.SpyInstance = jest.requireMock('../utils/infrastructure').generateBucketName;

jest.spyOn(fs, 'appendFileSync').mockImplementation(() => jest.fn());
jest.spyOn(fs, 'mkdirSync').mockImplementation();
setLogsDirectory('/config/logs/');

const cloudProvider = {
type: 'gcp' as const,
region: 'us-east1',
Expand Down
19 changes: 10 additions & 9 deletions packages/airnode-deployer/src/infrastructure/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import AdmZip from 'adm-zip';
import { AwsCloudProvider, GcpCloudProvider, loadTrustedConfig } from '@api3/airnode-node';
import * as aws from './aws';
import * as gcp from './gcp';
import { getSpinner } from '../utils/logger';
import { getSpinner, setLogsDirectory } from '../utils/logger';
import { parseSecretsFile } from '../utils';
import { Directory, DirectoryStructure } from '../utils/infrastructure';
import { mockBucketDirectoryStructure } from '../../test/fixtures';
Expand All @@ -20,6 +20,9 @@ jest.mock('../../package.json', () => ({

const exec = jest.fn();
jest.spyOn(util, 'promisify').mockImplementation(() => exec);
jest.spyOn(fs, 'appendFileSync').mockImplementation(() => jest.fn());
jest.spyOn(fs, 'mkdirSync').mockImplementation();
setLogsDirectory('/config/logs/');

import { version as nodeVersion } from '../../package.json';
import * as infrastructure from '.';
Expand Down Expand Up @@ -810,11 +813,9 @@ describe('deployAirnode', () => {
});

it(`throws an error if something in the deploy process wasn't successful`, async () => {
const expectedError = new Error('example error');
exec.mockRejectedValue(expectedError);

exec.mockRejectedValue('example error');
await expect(infrastructure.deployAirnode(config, configPath, secretsPath, Date.now())).rejects.toThrow(
expectedError.toString()
'Terraform error occurred. See deployer log files for more details.'
);
});
});
Expand Down Expand Up @@ -1131,10 +1132,10 @@ describe('removeAirnode', () => {
});

it('fails if the Terraform command fails', async () => {
const expectedError = new Error('example error');
exec.mockRejectedValue(expectedError);

await expect(infrastructure.removeAirnode(deploymentId)).rejects.toThrow(expectedError.toString());
exec.mockRejectedValue('example error');
await expect(infrastructure.removeAirnode(deploymentId)).rejects.toThrow(
'Terraform error occurred. See deployer log files for more details.'
);
});
});

Expand Down
Loading