"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.removeAirnode = exports.terraformAirnodeDestroy = exports.deployAirnode = exports.transformTerraformOutput = exports.terraformAirnodeApply = exports.terraformAirnodeInit = exports.prepareAirnodeApplyDestroyArguments = exports.prepareCloudProviderAirnodeApplyDestoryArguments = exports.prepareAirnodeInitArguments = exports.cloudProviderAirnodeImportOptions = exports.cloudProviderAirnodeInitArguments = exports.cloudProviderAirnodeApplyDestoryArguments = exports.gcpAirnodeImportOptions = exports.awsAirnodeImportOptions = exports.gcpAirnodeInitArguments = exports.awsAirnodeInitArguments = exports.gcpApplyDestroyArguments = exports.awsApplyDestroyArguments = exports.execTerraform = exports.runCommand = exports.TF_STATE_FILENAME = void 0;
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const util = __importStar(require("util"));
const child = __importStar(require("child_process"));
const path = __importStar(require("path"));
const airnode_node_1 = require("@api3/airnode-node");
const promise_utils_1 = require("@api3/promise-utils");
const compact_1 = __importDefault(require("lodash/compact"));
const isEmpty_1 = __importDefault(require("lodash/isEmpty"));
const omitBy_1 = __importDefault(require("lodash/omitBy"));
const isNil_1 = __importDefault(require("lodash/isNil"));
const aws = __importStar(require("./aws"));
const gcp = __importStar(require("./gcp"));
const logger = __importStar(require("../utils/logger"));
const infrastructure_1 = require("../utils/infrastructure");
const package_json_1 = require("../../package.json");
const utils_1 = require("../utils");
exports.TF_STATE_FILENAME = 'default.tfstate';
const exec = util.promisify(child.exec);
const handlerDir = path.resolve(`${__dirname}/../../.webpack`);
const terraformDir = path.resolve(`${__dirname}/../../terraform`);
function runCommand(command, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const stringifiedOptions = JSON.stringify(options);
        const commandSpinner = logger.debugSpinner(`Running command '${command}' with options ${stringifiedOptions}`);
        const goExec = yield (0, promise_utils_1.go)(() => exec(command, options));
        if (!goExec.success) {
            if (options.ignoreError) {
                if (logger.inDebugMode()) {
                    logger.getSpinner().info();
                    logger.warn(`Warning: ${goExec.error.message}`);
                }
                commandSpinner.warn(`Command '${command}' with options ${stringifiedOptions} failed`);
                return '';
            }
            logger.getSpinner().info();
            commandSpinner.fail(`Command '${command}' with options ${stringifiedOptions} failed`);
            throw (0, infrastructure_1.logAndReturnError)(goExec.error.toString());
        }
        commandSpinner.succeed(`Finished command '${command}' with options ${stringifiedOptions}`);
        return goExec.data.stdout;
    });
}
exports.runCommand = runCommand;
function execTerraform(execOptions, command, args, options) {
    const formattedArgs = (0, infrastructure_1.formatTerraformArguments)(args);
    const fullCommand = (0, compact_1.default)(['terraform', command, formattedArgs.join(' '), options === null || options === void 0 ? void 0 : options.join(' ')]).join(' ');
    return runCommand(fullCommand, execOptions);
}
exports.execTerraform = execTerraform;
function awsApplyDestroyArguments(cloudProvider, _bucket, _path) {
    return [['var', 'aws_region', cloudProvider.region]];
}
exports.awsApplyDestroyArguments = awsApplyDestroyArguments;
function gcpApplyDestroyArguments(cloudProvider, bucket, path) {
    return [
        ['var', 'gcp_region', cloudProvider.region],
        ['var', 'gcp_project', cloudProvider.projectId],
        ['var', 'airnode_bucket', bucket],
        ['var', 'deployment_bucket_dir', path],
    ];
}
exports.gcpApplyDestroyArguments = gcpApplyDestroyArguments;
function awsAirnodeInitArguments(cloudProvider, bucket, path) {
    return [
        ['backend-config', 'region', cloudProvider.region],
        ['backend-config', 'bucket', bucket],
        // This is the filename used by GCP and can't be configured there. Using it here as well to keep it consistent.
        ['backend-config', 'key', `${path}/${exports.TF_STATE_FILENAME}`],
    ];
}
exports.awsAirnodeInitArguments = awsAirnodeInitArguments;
function gcpAirnodeInitArguments(_cloudProvider, bucket, path) {
    return [
        ['backend-config', 'bucket', bucket],
        ['backend-config', 'prefix', path],
    ];
}
exports.gcpAirnodeInitArguments = gcpAirnodeInitArguments;
function awsAirnodeImportOptions(_cloudProvider) {
    return [];
}
exports.awsAirnodeImportOptions = awsAirnodeImportOptions;
function gcpAirnodeImportOptions(cloudProvider) {
    return ['module.startCoordinator.google_app_engine_application.app[0]', cloudProvider.projectId];
}
exports.gcpAirnodeImportOptions = gcpAirnodeImportOptions;
const cloudProviderLib = {
    aws: aws,
    gcp: gcp,
};
exports.cloudProviderAirnodeApplyDestoryArguments = {
    aws: awsApplyDestroyArguments,
    gcp: gcpApplyDestroyArguments,
};
exports.cloudProviderAirnodeInitArguments = {
    aws: awsAirnodeInitArguments,
    gcp: gcpAirnodeInitArguments,
};
exports.cloudProviderAirnodeImportOptions = {
    aws: awsAirnodeImportOptions,
    gcp: gcpAirnodeImportOptions,
};
function prepareAirnodeInitArguments(cloudProvider, bucket, path, commonArguments) {
    return [
        ...exports.cloudProviderAirnodeInitArguments[cloudProvider.type](cloudProvider, bucket, path),
        ...commonArguments,
    ];
}
exports.prepareAirnodeInitArguments = prepareAirnodeInitArguments;
function prepareCloudProviderAirnodeApplyDestoryArguments(cloudProvider, bucket, path, commonArguments) {
    return [
        ...exports.cloudProviderAirnodeApplyDestoryArguments[cloudProvider.type](cloudProvider, bucket, path),
        ...commonArguments,
    ];
}
exports.prepareCloudProviderAirnodeApplyDestoryArguments = prepareCloudProviderAirnodeApplyDestoryArguments;
function prepareAirnodeApplyDestroyArguments(variables) {
    const { airnodeAddressShort, stage, configPath, secretsPath, handlerDir, disableConcurrencyReservations, airnodeWalletPrivateKey, } = variables;
    return [
        ['var', 'airnode_address_short', airnodeAddressShort],
        ['var', 'stage', stage],
        ['var', 'configuration_file', configPath ? path.resolve(configPath) : 'NULL'],
        ['var', 'secrets_file', secretsPath ? path.resolve(secretsPath) : 'NULL'],
        ['var', 'handler_dir', handlerDir],
        ['var', 'disable_concurrency_reservation', `${!!disableConcurrencyReservations}`],
        ['var', 'airnode_wallet_private_key', airnodeWalletPrivateKey ? airnodeWalletPrivateKey : 'NULL'],
        ['input', 'false'],
        'no-color',
    ];
}
exports.prepareAirnodeApplyDestroyArguments = prepareAirnodeApplyDestroyArguments;
function terraformAirnodeInit(execOptions, cloudProvider, bucket, bucketPath) {
    return __awaiter(this, void 0, void 0, function* () {
        const terraformCloudProviderDirectory = path.join(terraformDir, cloudProvider.type);
        const commonArguments = [['from-module', terraformCloudProviderDirectory]];
        yield execTerraform(execOptions, 'init', prepareAirnodeInitArguments(cloudProvider, bucket, bucketPath, commonArguments));
    });
}
exports.terraformAirnodeInit = terraformAirnodeInit;
function terraformAirnodeApply(execOptions, config, bucket, bucketPath, configPath, secretsPath) {
    return __awaiter(this, void 0, void 0, function* () {
        const { airnodeWalletMnemonic, stage, httpGateway, httpSignedDataGateway } = config.nodeSettings;
        const cloudProvider = config.nodeSettings.cloudProvider;
        const airnodeAddressShort = (0, utils_1.shortenAirnodeAddress)((0, utils_1.deriveAirnodeAddress)(airnodeWalletMnemonic));
        const airnodeWalletPrivateKey = airnode_node_1.evm.getAirnodeWallet(config).privateKey;
        const maxConcurrency = config.chains.reduce((concurrency, chain) => concurrency + chain.maxConcurrency, 0);
        yield terraformAirnodeInit(execOptions, cloudProvider, bucket, bucketPath);
        const commonArguments = prepareAirnodeApplyDestroyArguments({
            airnodeAddressShort,
            stage,
            configPath,
            secretsPath,
            handlerDir,
            disableConcurrencyReservations: cloudProvider.disableConcurrencyReservations,
            airnodeWalletPrivateKey,
        });
        commonArguments.push(['var', 'max_concurrency', `${maxConcurrency}`]);
        if (httpGateway === null || httpGateway === void 0 ? void 0 : httpGateway.enabled) {
            commonArguments.push(['var', 'http_api_key', httpGateway.apiKey]);
            if (httpGateway.maxConcurrency) {
                commonArguments.push(['var', 'http_max_concurrency', `${httpGateway.maxConcurrency}`]);
            }
        }
        if (httpSignedDataGateway === null || httpSignedDataGateway === void 0 ? void 0 : httpSignedDataGateway.enabled) {
            commonArguments.push(['var', 'http_signed_data_api_key', httpSignedDataGateway.apiKey]);
            if (httpSignedDataGateway.maxConcurrency) {
                commonArguments.push(['var', 'http_signed_data_max_concurrency', `${httpSignedDataGateway.maxConcurrency}`]);
            }
        }
        const importOptions = exports.cloudProviderAirnodeImportOptions[cloudProvider.type](cloudProvider);
        if (!(0, isEmpty_1.default)(importOptions)) {
            yield execTerraform(Object.assign(Object.assign({}, execOptions), { ignoreError: true }), 'import', prepareCloudProviderAirnodeApplyDestoryArguments(cloudProvider, bucket, bucketPath, commonArguments), exports.cloudProviderAirnodeImportOptions[cloudProvider.type](cloudProvider));
        }
        commonArguments.push('auto-approve');
        yield execTerraform(execOptions, 'apply', prepareCloudProviderAirnodeApplyDestoryArguments(cloudProvider, bucket, bucketPath, commonArguments));
    });
}
exports.terraformAirnodeApply = terraformAirnodeApply;
function transformTerraformOutput(terraformOutput) {
    var _a, _b;
    const parsedOutput = JSON.parse(terraformOutput);
    return (0, omitBy_1.default)({
        httpGatewayUrl: (_a = parsedOutput.http_gateway_url) === null || _a === void 0 ? void 0 : _a.value,
        httpSignedDataGatewayUrl: (_b = parsedOutput.http_signed_data_gateway_url) === null || _b === void 0 ? void 0 : _b.value,
    }, isNil_1.default);
}
exports.transformTerraformOutput = transformTerraformOutput;
const deployAirnode = (config, configPath, secretsPath) => __awaiter(void 0, void 0, void 0, function* () {
    const { airnodeWalletMnemonic, cloudProvider, stage } = config.nodeSettings;
    const airnodeAddress = (0, utils_1.deriveAirnodeAddress)(airnodeWalletMnemonic);
    const { type, region } = cloudProvider;
    const spinner = logger.getSpinner().start(`Deploying Airnode ${airnodeAddress} ${stage} to ${type} ${region}`);
    if (logger.inDebugMode()) {
        spinner.info();
    }
    const goDeploy = yield (0, promise_utils_1.go)(() => __awaiter(void 0, void 0, void 0, function* () {
        logger.debug('Fetching Airnode bucket');
        let bucketName = yield cloudProviderLib[type].getAirnodeBucket(cloudProvider);
        if (!bucketName) {
            logger.debug('No Airnode bucket found, creating');
            bucketName = yield cloudProviderLib[type].createAirnodeBucket(cloudProvider);
        }
        logger.debug(`Using Airnode bucket '${bucketName}'`);
        logger.debug('Fetching Airnode bucket content');
        const directoryStructure = yield cloudProviderLib[type].getBucketDirectoryStructure(cloudProvider, bucketName);
        const timestamp = Date.now();
        const bucketStagePath = `${airnodeAddress}/${stage}`;
        const bucketDeploymentPath = `${bucketStagePath}/${timestamp}`;
        const stageDirectory = (0, infrastructure_1.getStageDirectory)(directoryStructure, airnodeAddress, stage);
        if (stageDirectory) {
            logger.debug(`Deployment '${bucketStagePath}' already exists`);
            const latestDeployment = Object.keys(stageDirectory.children).sort().reverse()[0];
            const bucketConfigPath = `${bucketStagePath}/${latestDeployment}/config.json`;
            logger.debug(`Fetching configuration file '${bucketConfigPath}'`);
            const remoteConfig = JSON.parse(yield cloudProviderLib[type].getFileFromBucket(cloudProvider, bucketName, bucketConfigPath));
            const remoteNodeSettings = remoteConfig.nodeSettings;
            const remoteCloudProvider = remoteNodeSettings.cloudProvider;
            if (remoteNodeSettings.nodeVersion !== package_json_1.version) {
                throw new Error(`Can't update an Airnode deployment with airnode-deployer of a different version. Deployed version: ${remoteNodeSettings.nodeVersion}, airnode-deployer version: ${package_json_1.version}`);
            }
            if (remoteCloudProvider.region !== region) {
                throw new Error(`Can't change a region of an already deployed Airnode. Current region: ${remoteCloudProvider.region}, new region: ${region}`);
            }
            logger.debug(`Copying Terraform state file for new deployment ${bucketDeploymentPath}`);
            const latestBucketTerraformStatePath = `${bucketStagePath}/${latestDeployment}/${exports.TF_STATE_FILENAME}`;
            const newBucketTerraformStatePath = `${bucketDeploymentPath}/${exports.TF_STATE_FILENAME}`;
            yield cloudProviderLib[type].copyFileInBucket(cloudProvider, bucketName, latestBucketTerraformStatePath, newBucketTerraformStatePath);
        }
        logger.debug(`Storing configuration file for new deployment ${bucketDeploymentPath}`);
        const bucketConfigPath = `${bucketDeploymentPath}/config.json`;
        yield cloudProviderLib[type].storeFileToBucket(cloudProvider, bucketName, bucketConfigPath, configPath);
        logger.debug(`Storing secrets file for new deployment ${bucketDeploymentPath}`);
        const bucketSecretsPath = `${bucketDeploymentPath}/secrets.env`;
        yield cloudProviderLib[type].storeFileToBucket(cloudProvider, bucketName, bucketSecretsPath, secretsPath);
        logger.debug('Deploying Airnode via Terraform recipes');
        const airnodeTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'airnode'));
        const execOptions = { cwd: airnodeTmpDir };
        yield terraformAirnodeApply(execOptions, config, bucketName, bucketDeploymentPath, configPath, secretsPath);
        const output = yield execTerraform(execOptions, 'output', ['json', 'no-color']);
        fs.rmSync(airnodeTmpDir, { recursive: true });
        return output;
    }));
    if (!goDeploy.success) {
        spinner.fail(`Failed deploying Airnode ${airnodeAddress} ${stage} to ${type} ${region}`);
        throw goDeploy.error;
    }
    spinner.succeed(`Deployed Airnode ${airnodeAddress} ${stage} to ${type} ${region}`);
    return transformTerraformOutput(goDeploy.data);
});
exports.deployAirnode = deployAirnode;
function terraformAirnodeDestroy(execOptions, cloudProvider, airnodeAddressShort, stage, bucket, bucketPath) {
    return __awaiter(this, void 0, void 0, function* () {
        yield terraformAirnodeInit(execOptions, cloudProvider, bucket, bucketPath);
        const commonArguments = prepareAirnodeApplyDestroyArguments({
            airnodeAddressShort,
            stage,
            handlerDir,
            disableConcurrencyReservations: cloudProvider.disableConcurrencyReservations,
        });
        commonArguments.push('auto-approve');
        yield execTerraform(execOptions, 'destroy', prepareCloudProviderAirnodeApplyDestoryArguments(cloudProvider, bucket, bucketPath, commonArguments));
    });
}
exports.terraformAirnodeDestroy = terraformAirnodeDestroy;
function removeAirnode(airnodeAddress, stage, cloudProvider) {
    return __awaiter(this, void 0, void 0, function* () {
        const { type, region } = cloudProvider;
        const spinner = logger.getSpinner().start(`Removing Airnode ${airnodeAddress} ${stage} from ${type} ${region}`);
        if (logger.inDebugMode()) {
            spinner.info();
        }
        const goRemove = yield (0, promise_utils_1.go)(() => __awaiter(this, void 0, void 0, function* () {
            logger.debug('Fetching Airnode bucket');
            const bucketName = yield cloudProviderLib[type].getAirnodeBucket(cloudProvider);
            if (!bucketName) {
                throw new Error(`There's no Airnode bucket available`);
            }
            logger.debug('Fetching Airnode bucket content');
            let directoryStructure = yield cloudProviderLib[type].getBucketDirectoryStructure(cloudProvider, bucketName);
            let addressDirectory = (0, infrastructure_1.getAddressDirectory)(directoryStructure, airnodeAddress);
            let stageDirectory = (0, infrastructure_1.getStageDirectory)(directoryStructure, airnodeAddress, stage);
            if (!addressDirectory || !stageDirectory) {
                throw new Error(`There's no Airnode deployment with address '${airnodeAddress}' and stage '${stage}'`);
            }
            const latestDeployment = Object.keys(stageDirectory.children).sort().reverse()[0];
            const bucketLatestDeploymentPath = `${airnodeAddress}/${stage}/${latestDeployment}`;
            const bucketConfigPath = `${bucketLatestDeploymentPath}/config.json`;
            logger.debug(`Fetching configuration file '${bucketConfigPath}'`);
            const config = JSON.parse(yield cloudProviderLib[type].getFileFromBucket(cloudProvider, bucketName, bucketConfigPath));
            const remoteNodeSettings = config.nodeSettings;
            const remoteCloudProvider = remoteNodeSettings.cloudProvider;
            if (remoteNodeSettings.nodeVersion !== package_json_1.version) {
                throw new Error(`Can't remove an Airnode deployment with airnode-deployer of a different version. Deployed version: ${remoteNodeSettings.nodeVersion}, airnode-deployer version: ${package_json_1.version}`);
            }
            if (remoteCloudProvider.region !== region) {
                throw new Error(`Can't remove an Airnode deployment from specified region. Airnode region: ${remoteCloudProvider.region}, specified region: ${region}`);
            }
            logger.debug('Removing Airnode via Terraform recipes');
            const airnodeAddressShort = (0, utils_1.shortenAirnodeAddress)(airnodeAddress);
            const airnodeTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'airnode'));
            const execOptions = { cwd: airnodeTmpDir };
            yield terraformAirnodeDestroy(execOptions, cloudProvider, airnodeAddressShort, stage, bucketName, bucketLatestDeploymentPath);
            fs.rmSync(airnodeTmpDir, { recursive: true });
            // Refreshing the bucket content because the source code archives were removed by Terraform
            logger.debug('Refreshing Airnode bucket content');
            directoryStructure = yield cloudProviderLib[type].getBucketDirectoryStructure(cloudProvider, bucketName);
            addressDirectory = (0, infrastructure_1.getAddressDirectory)(directoryStructure, airnodeAddress);
            stageDirectory = (0, infrastructure_1.getStageDirectory)(directoryStructure, airnodeAddress, stage);
            // Delete stage directory and its content
            logger.debug(`Deleting deployment directory '${stageDirectory.bucketKey}' and its content`);
            yield cloudProviderLib[type].deleteBucketDirectory(cloudProvider, bucketName, stageDirectory);
            // eslint-disable-next-line functional/immutable-data
            delete addressDirectory.children[stage];
            // Delete Airnode address directory if empty
            if (Object.keys(addressDirectory.children).length === 0) {
                logger.debug(`Deleting Airnode address directory '${addressDirectory.bucketKey}'`);
                yield cloudProviderLib[type].deleteBucketDirectory(cloudProvider, bucketName, addressDirectory);
                // eslint-disable-next-line functional/immutable-data
                delete directoryStructure[airnodeAddress];
            }
            // Delete the whole bucket if empty
            if (Object.keys(directoryStructure).length === 0) {
                logger.debug(`Deleting Airnode bucket '${bucketName}'`);
                yield cloudProviderLib[type].deleteBucket(cloudProvider, bucketName);
            }
        }));
        if (!goRemove.success) {
            spinner.fail(`Failed to remove Airnode ${airnodeAddress} ${stage} from ${type} ${region}`);
            throw goRemove.error;
        }
        spinner.succeed(`Removed Airnode ${airnodeAddress} ${stage} from ${type} ${region}`);
    });
}
exports.removeAirnode = removeAirnode;
//# sourceMappingURL=index.js.map