"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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveDeploymentFiles = exports.fetchFiles = exports.deploymentInfo = exports.listAirnodes = 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 dotenv = __importStar(require("dotenv"));
const adm_zip_1 = __importDefault(require("adm-zip"));
const airnode_node_1 = require("@api3/airnode-node");
const promise_utils_1 = require("@api3/promise-utils");
const airnode_validator_1 = require("@api3/airnode-validator");
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 sortBy_1 = __importDefault(require("lodash/sortBy"));
const cli_table3_1 = __importDefault(require("cli-table3"));
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");
const cli_1 = require("../utils/cli");
exports.TF_STATE_FILENAME = 'default.tfstate';
const exec = util.promisify(child.exec);
const handlerDir = path.resolve(`${__dirname}/../../.webpack`);
const terraformDir = path.resolve(`${__dirname}/../../terraform`);
async function runCommand(command, options) {
    const stringifiedOptions = JSON.stringify(options);
    const commandSpinner = logger.debugSpinner(`Running command '${command}' with options ${stringifiedOptions}`);
    const goExec = await (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;
async function execTerraform(execOptions, command, args, options) {
    const formattedArgs = (0, infrastructure_1.formatTerraformArguments)(args);
    const fullCommand = (0, compact_1.default)(['terraform', command, formattedArgs.join(' '), options?.join(' ')]).join(' ');
    const goRunCommand = await (0, promise_utils_1.go)(() => runCommand(fullCommand, execOptions));
    if (!goRunCommand.success) {
        throw new Error('Terraform error occurred. See deployer log files for more details.');
    }
    return goRunCommand.data;
}
exports.execTerraform = execTerraform;
function awsApplyDestroyArguments(cloudProvider, _bucket, _bucketDeploymentPath) {
    return [['var', 'aws_region', cloudProvider.region]];
}
exports.awsApplyDestroyArguments = awsApplyDestroyArguments;
function gcpApplyDestroyArguments(cloudProvider, bucket, bucketDeploymentPath) {
    return [
        ['var', 'gcp_region', cloudProvider.region],
        ['var', 'gcp_project', cloudProvider.projectId],
        ['var', 'airnode_bucket', bucket.name],
        ['var', 'deployment_bucket_dir', bucketDeploymentPath],
    ];
}
exports.gcpApplyDestroyArguments = gcpApplyDestroyArguments;
function awsAirnodeInitArguments(bucket, bucketDeploymentPath) {
    return [
        ['backend-config', 'region', bucket.region],
        ['backend-config', 'bucket', bucket.name],
        // 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', `${bucketDeploymentPath}/${exports.TF_STATE_FILENAME}`],
    ];
}
exports.awsAirnodeInitArguments = awsAirnodeInitArguments;
function gcpAirnodeInitArguments(bucket, bucketDeploymentPath) {
    return [
        ['backend-config', 'bucket', bucket.name],
        ['backend-config', 'prefix', bucketDeploymentPath],
    ];
}
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, bucketDeploymentPath, commonArguments) {
    return [...exports.cloudProviderAirnodeInitArguments[cloudProvider.type](bucket, bucketDeploymentPath), ...commonArguments];
}
exports.prepareAirnodeInitArguments = prepareAirnodeInitArguments;
function prepareCloudProviderAirnodeApplyDestoryArguments(cloudProvider, bucket, bucketDeploymentPath, commonArguments) {
    return [
        ...exports.cloudProviderAirnodeApplyDestoryArguments[cloudProvider.type](cloudProvider, bucket, bucketDeploymentPath),
        ...commonArguments,
    ];
}
exports.prepareCloudProviderAirnodeApplyDestoryArguments = prepareCloudProviderAirnodeApplyDestoryArguments;
function prepareAirnodeApplyDestroyArguments(variables) {
    const { deploymentId, configPath, secretsPath, handlerDir, disableConcurrencyReservations, airnodeWalletPrivateKey } = variables;
    return [
        ['var', 'deployment_id', deploymentId],
        ['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;
async function terraformAirnodeInit(execOptions, cloudProvider, bucket, bucketDeploymentPath) {
    const terraformCloudProviderDirectory = path.join(terraformDir, cloudProvider.type);
    const commonArguments = [['from-module', terraformCloudProviderDirectory]];
    await execTerraform(execOptions, 'init', prepareAirnodeInitArguments(cloudProvider, bucket, bucketDeploymentPath, commonArguments));
}
exports.terraformAirnodeInit = terraformAirnodeInit;
async function terraformAirnodeApply(execOptions, config, bucket, bucketDeploymentPath, configPath, secretsPath) {
    const { airnodeWalletMnemonic, stage, httpGateway, httpSignedDataGateway, oevGateway, nodeVersion: configNodeVersion, } = config.nodeSettings;
    const cloudProvider = config.nodeSettings.cloudProvider;
    const airnodeAddress = (0, utils_1.deriveAirnodeAddress)(airnodeWalletMnemonic);
    const airnodeWalletPrivateKey = airnode_node_1.evm.getAirnodeWallet(config).privateKey;
    logger.setSecret(airnodeWalletPrivateKey);
    const maxConcurrency = config.chains.reduce((concurrency, chain) => concurrency + chain.maxConcurrency, 0);
    await terraformAirnodeInit(execOptions, cloudProvider, bucket, bucketDeploymentPath);
    const commonArguments = prepareAirnodeApplyDestroyArguments({
        deploymentId: (0, airnode_node_1.deriveDeploymentId)(cloudProvider, airnodeAddress, stage, configNodeVersion),
        configPath,
        secretsPath,
        handlerDir,
        disableConcurrencyReservations: cloudProvider.disableConcurrencyReservations,
        airnodeWalletPrivateKey,
    });
    commonArguments.push(['var', 'max_concurrency', `${maxConcurrency}`]);
    if (httpGateway.enabled) {
        commonArguments.push(['var', 'http_gateway_enabled', 'true']);
        if (httpGateway.maxConcurrency) {
            commonArguments.push(['var', 'http_max_concurrency', `${httpGateway.maxConcurrency}`]);
        }
    }
    if (httpSignedDataGateway.enabled) {
        commonArguments.push(['var', 'http_signed_data_gateway_enabled', 'true']);
        if (httpSignedDataGateway.maxConcurrency) {
            commonArguments.push(['var', 'http_signed_data_max_concurrency', `${httpSignedDataGateway.maxConcurrency}`]);
        }
    }
    if (oevGateway.enabled) {
        commonArguments.push(['var', 'oev_gateway_enabled', 'true']);
        if (oevGateway.maxConcurrency) {
            commonArguments.push(['var', 'oev_max_concurrency', `${oevGateway.maxConcurrency}`]);
        }
    }
    const importOptions = exports.cloudProviderAirnodeImportOptions[cloudProvider.type](cloudProvider);
    if (!(0, isEmpty_1.default)(importOptions)) {
        await execTerraform({ ...execOptions, ignoreError: true }, 'import', prepareCloudProviderAirnodeApplyDestoryArguments(cloudProvider, bucket, bucketDeploymentPath, commonArguments), exports.cloudProviderAirnodeImportOptions[cloudProvider.type](cloudProvider));
    }
    commonArguments.push('auto-approve');
    await execTerraform(execOptions, 'apply', prepareCloudProviderAirnodeApplyDestoryArguments(cloudProvider, bucket, bucketDeploymentPath, commonArguments));
}
exports.terraformAirnodeApply = terraformAirnodeApply;
function transformTerraformOutput(terraformOutput) {
    const parsedOutput = JSON.parse(terraformOutput);
    return (0, omitBy_1.default)({
        httpGatewayUrl: parsedOutput.http_gateway_url?.value,
        httpSignedDataGatewayUrl: parsedOutput.http_signed_data_gateway_url?.value,
        oevGatewayUrl: parsedOutput.oev_gateway_url?.value,
    }, isNil_1.default);
}
exports.transformTerraformOutput = transformTerraformOutput;
const deployAirnode = async (config, configPath, secretsPath, timestamp) => {
    const { airnodeWalletMnemonic, cloudProvider, stage } = config.nodeSettings;
    const airnodeAddress = (0, utils_1.deriveAirnodeAddress)(airnodeWalletMnemonic);
    const { type, region } = cloudProvider;
    const spinner = logger.getSpinner();
    spinner.start(`Deploying Airnode ${airnodeAddress} ${stage} to ${type} ${region}`);
    if (logger.inDebugMode()) {
        spinner.info();
    }
    const goDeploy = await (0, promise_utils_1.go)(async () => {
        logger.debug('Fetching Airnode bucket');
        let bucket = await cloudProviderLib[type].getAirnodeBucket();
        if (!bucket) {
            logger.debug('No Airnode bucket found, creating');
            bucket = await cloudProviderLib[type].createAirnodeBucket(cloudProvider);
        }
        logger.debug(`Using Airnode bucket '${bucket.name}'`);
        logger.debug('Fetching Airnode bucket content');
        const directoryStructure = await cloudProviderLib[type].getBucketDirectoryStructure(bucket);
        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 bucketMissingFiles = (0, infrastructure_1.getMissingBucketFiles)(directoryStructure);
            if (bucketMissingFiles[airnodeAddress] &&
                bucketMissingFiles[airnodeAddress][stage] &&
                bucketMissingFiles[airnodeAddress][stage].length !== 0) {
                throw new Error(`Can't update an Airnode with missing files: ${bucketMissingFiles[airnodeAddress][stage].join(', ')}. Deployer commands may fail and manual removal may be necessary.`);
            }
            const latestDeployment = Object.keys(stageDirectory.children).sort().reverse()[0];
            const bucketConfigPath = `${bucketStagePath}/${latestDeployment}/config.json`;
            logger.debug(`Fetching configuration file '${bucketConfigPath}'`);
            const goGetRemoteConfigFileFromBucket = await (0, promise_utils_1.go)(() => cloudProviderLib[type].getFileFromBucket(bucket, bucketConfigPath));
            if (!goGetRemoteConfigFileFromBucket.success) {
                throw new Error(`Failed to fetch configuration file. Error: ${goGetRemoteConfigFileFromBucket.error.message}`);
            }
            const goRemoteConfig = (0, promise_utils_1.goSync)(() => JSON.parse(goGetRemoteConfigFileFromBucket.data));
            if (!goRemoteConfig.success) {
                throw new Error(`Failed to parse configuration file. Error: ${goRemoteConfig.error.message}`);
            }
            const remoteNodeSettings = goRemoteConfig.data.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}`;
            await cloudProviderLib[type].copyFileInBucket(bucket, latestBucketTerraformStatePath, newBucketTerraformStatePath);
        }
        logger.debug(`Storing configuration file for new deployment ${bucketDeploymentPath}`);
        const bucketConfigPath = `${bucketDeploymentPath}/config.json`;
        await cloudProviderLib[type].storeFileToBucket(bucket, bucketConfigPath, configPath);
        logger.debug(`Storing secrets file for new deployment ${bucketDeploymentPath}`);
        const bucketSecretsPath = `${bucketDeploymentPath}/secrets.env`;
        await cloudProviderLib[type].storeFileToBucket(bucket, bucketSecretsPath, secretsPath);
        logger.debug('Deploying Airnode via Terraform recipes');
        const airnodeTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'airnode'));
        const execOptions = { cwd: airnodeTmpDir };
        await terraformAirnodeApply(execOptions, config, bucket, bucketDeploymentPath, configPath, secretsPath);
        const output = await 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;
async function terraformAirnodeDestroy(execOptions, cloudProvider, deploymentId, bucket, bucketDeploymentPath) {
    await terraformAirnodeInit(execOptions, cloudProvider, bucket, bucketDeploymentPath);
    const commonArguments = prepareAirnodeApplyDestroyArguments({
        deploymentId,
        handlerDir,
        disableConcurrencyReservations: cloudProvider.disableConcurrencyReservations,
    });
    commonArguments.push('auto-approve');
    await execTerraform(execOptions, 'destroy', prepareCloudProviderAirnodeApplyDestoryArguments(cloudProvider, bucket, bucketDeploymentPath, commonArguments));
}
exports.terraformAirnodeDestroy = terraformAirnodeDestroy;
// If deploymentId is provided it tries to fetch only that one deployment and returns early when found
async function fetchDeployments(cloudProviderType, deploymentId) {
    const deployments = [];
    const bucket = await cloudProviderLib[cloudProviderType].getAirnodeBucket();
    if (!bucket) {
        logger.debug(`No deployments available on ${cloudProviderType.toUpperCase()}`);
        return deployments;
    }
    const directoryStructure = await cloudProviderLib[cloudProviderType].getBucketDirectoryStructure(bucket);
    const bucketMissingFiles = (0, infrastructure_1.getMissingBucketFiles)(directoryStructure);
    for (const [airnodeAddress, addressDirectory] of Object.entries(directoryStructure)) {
        if (addressDirectory.type !== infrastructure_1.FileSystemType.Directory) {
            logger.warn(`Invalid item in bucket '${bucket.name}' (${cloudProviderType.toUpperCase()}) with key '${addressDirectory.bucketKey}'. Skipping.`);
            continue;
        }
        for (const [stage, stageDirectory] of Object.entries(addressDirectory.children)) {
            if (stageDirectory.type !== infrastructure_1.FileSystemType.Directory) {
                logger.warn(`Invalid item in bucket '${bucket.name}' (${cloudProviderType.toUpperCase()}) with key '${stageDirectory.bucketKey}'. Skipping.`);
                continue;
            }
            const latestDeployment = Object.keys(stageDirectory.children).sort().reverse()[0];
            if (bucketMissingFiles[airnodeAddress] &&
                bucketMissingFiles[airnodeAddress][stage] &&
                bucketMissingFiles[airnodeAddress][stage].length !== 0) {
                continue;
            }
            const bucketLatestDeploymentPath = `${airnodeAddress}/${stage}/${latestDeployment}`;
            const bucketConfigPath = `${bucketLatestDeploymentPath}/config.json`;
            logger.debug(`Fetching configuration file '${bucketConfigPath}'`);
            const goGetConfigFileFromBucket = await (0, promise_utils_1.go)(() => cloudProviderLib[cloudProviderType].getFileFromBucket(bucket, bucketConfigPath));
            if (!goGetConfigFileFromBucket.success) {
                logger.warn(`Failed to fetch configuration file. Error: ${goGetConfigFileFromBucket.error.message} Skipping.`);
                continue;
            }
            const goConfig = (0, promise_utils_1.goSync)(() => JSON.parse(goGetConfigFileFromBucket.data));
            if (!goConfig.success) {
                logger.warn(`Failed to parse configuration file. Error: ${goConfig.error.message} Skipping.`);
                continue;
            }
            logger.debug(`Fetching secrets file '${bucketConfigPath}'`);
            const bucketSecretsPath = `${bucketLatestDeploymentPath}/secrets.env`;
            const goGetSecretsFileFromBucket = await (0, promise_utils_1.go)(() => cloudProviderLib[cloudProviderType].getFileFromBucket(bucket, bucketSecretsPath));
            if (!goGetSecretsFileFromBucket.success) {
                logger.warn(`Failed to fetch secrets file. Error: ${goGetSecretsFileFromBucket.error.message} Skipping.`);
                continue;
            }
            const secrets = dotenv.parse(goGetSecretsFileFromBucket.data);
            const interpolatedConfig = (0, airnode_validator_1.unsafeParseConfigWithSecrets)(goConfig.data, secrets);
            const cloudProvider = interpolatedConfig.nodeSettings.cloudProvider;
            const airnodeVersion = interpolatedConfig.nodeSettings.nodeVersion;
            const id = (0, airnode_node_1.deriveDeploymentId)(cloudProvider, airnodeAddress, stage, airnodeVersion);
            const deploymentVersions = Object.keys(stageDirectory.children).map((versionTimestamp) => ({
                id: (0, airnode_node_1.deriveDeploymentVersionId)(cloudProvider, airnodeAddress, stage, airnodeVersion, versionTimestamp),
                timestamp: versionTimestamp,
            }));
            const deployment = {
                id,
                cloudProvider,
                airnodeAddress,
                stage,
                airnodeVersion,
                lastUpdate: latestDeployment,
                versions: deploymentVersions,
                bucket,
                bucketLatestDeploymentPath,
            };
            // We're looking for just one deployment
            if (deploymentId) {
                if (deploymentId === id) {
                    // Return the deployment if found
                    return [deployment];
                }
                else {
                    // Keeping list of the deployments empty if we're looking for just one
                    continue;
                }
            }
            deployments.push(deployment);
        }
    }
    return deployments;
}
async function fetchDeployment(cloudProviderType, deploymentId, versionId) {
    // We need to call `fetchDeployments` even if we want just one deployment due to the way the deployment ID is build
    // For that reason we keep the `deploymentId` paramater in `fetchDeployments` that allows to return early if the deployment is found
    const goDeployemnts = await (0, promise_utils_1.go)(() => fetchDeployments(cloudProviderType, deploymentId));
    if (!goDeployemnts.success) {
        throw new Error(`Failed to fetch info about '${deploymentId}' from ${cloudProviderType.toUpperCase()}: ${goDeployemnts.error}`);
    }
    if (goDeployemnts.data.length === 0) {
        throw new Error(`No deployment with ID '${deploymentId}' found`);
    }
    const deployment = goDeployemnts.data[0];
    let requestedVersion;
    if (versionId) {
        requestedVersion = deployment.versions.find((version) => version.id === versionId);
        if (!requestedVersion) {
            throw new Error(`No deployment with ID '${deploymentId}' and version '${versionId}' found`);
        }
    }
    const version = requestedVersion ?? (0, sortBy_1.default)(deployment.versions, 'timestamp').reverse()[0];
    return { deployment, version };
}
async function downloadDeploymentFiles(cloudProviderType, deployment, version) {
    const { airnodeAddress, stage, bucket } = deployment;
    const deploymentPathPrefix = `${airnodeAddress}/${stage}/${version.timestamp}`;
    const configFileBucketPath = `${deploymentPathPrefix}/config.json`;
    const secretsFileBucketPath = `${deploymentPathPrefix}/secrets.env`;
    const configContent = await cloudProviderLib[cloudProviderType].getFileFromBucket(bucket, configFileBucketPath);
    const secretsContent = await cloudProviderLib[cloudProviderType].getFileFromBucket(bucket, secretsFileBucketPath);
    return { configContent, secretsContent };
}
async function removeAirnode(deploymentId) {
    const cloudProviderType = deploymentId.slice(0, 3);
    if (!airnode_node_1.availableCloudProviders.includes(cloudProviderType)) {
        throw new Error(`Invalid deployment ID '${deploymentId}'`);
    }
    const spinner = logger.getSpinner();
    spinner.start(`Removing Airnode '${deploymentId}'`);
    if (logger.inDebugMode()) {
        spinner.info();
    }
    const goRemove = await (0, promise_utils_1.go)(async () => {
        const goFetchDeployment = await (0, promise_utils_1.go)(() => fetchDeployment(cloudProviderType, deploymentId));
        if (!goFetchDeployment.success) {
            spinner.stop();
            throw goFetchDeployment.error;
        }
        const { deployment } = goFetchDeployment.data;
        const { cloudProvider, airnodeAddress, stage, airnodeVersion: deployedVersion, bucket, bucketLatestDeploymentPath, } = deployment;
        if (deployedVersion !== package_json_1.version) {
            throw new Error(`Can't remove an Airnode deployment with airnode-deployer of a different version. Deployed version: ${deployedVersion}, airnode-deployer version: ${package_json_1.version}`);
        }
        logger.debug('Removing Airnode via Terraform recipes');
        const airnodeTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'airnode'));
        const execOptions = { cwd: airnodeTmpDir };
        await terraformAirnodeDestroy(execOptions, cloudProvider, deploymentId, bucket, 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');
        const directoryStructure = await cloudProviderLib[cloudProviderType].getBucketDirectoryStructure(bucket);
        const addressDirectory = (0, infrastructure_1.getAddressDirectory)(directoryStructure, airnodeAddress);
        const stageDirectory = (0, infrastructure_1.getStageDirectory)(directoryStructure, airnodeAddress, stage);
        // Delete stage directory and its content
        logger.debug(`Deleting deployment directory '${stageDirectory.bucketKey}' and its content`);
        await cloudProviderLib[cloudProviderType].deleteBucketDirectory(bucket, 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}'`);
            await cloudProviderLib[cloudProviderType].deleteBucketDirectory(bucket, 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 '${bucket.name}'`);
            await cloudProviderLib[cloudProviderType].deleteBucket(bucket);
        }
    });
    if (!goRemove.success) {
        spinner.fail(`Failed to remove Airnode '${deploymentId}'`);
        throw goRemove.error;
    }
    spinner.succeed(`Airnode '${deploymentId}' removed successfully`);
}
exports.removeAirnode = removeAirnode;
async function listAirnodes(cloudProviders) {
    const deployments = [];
    for (const cloudProviderType of cloudProviders) {
        // Using different line of text for each cloud provider so we can easily convey which cloud provider failed
        // and which succeeded
        const spinner = logger.getSpinner();
        spinner.start(`Listing Airnode deployments from cloud provider ${cloudProviderType.toUpperCase()}`);
        if (logger.inDebugMode()) {
            spinner.info();
        }
        const goListCloudAirnodes = await (0, promise_utils_1.go)(() => fetchDeployments(cloudProviderType));
        if (goListCloudAirnodes.success) {
            spinner.succeed();
            deployments.push(...goListCloudAirnodes.data);
        }
        else {
            spinner.fail(`Failed to fetch deployments from ${cloudProviderType.toUpperCase()}: ${goListCloudAirnodes.error}`);
        }
    }
    deployments.sort(infrastructure_1.deploymentComparator);
    const table = new cli_table3_1.default({
        head: ['Deployment ID', 'Cloud provider', 'Airnode address', 'Stage', 'Airnode version', 'Last update'],
        style: {
            head: ['bold'],
        },
    });
    table.push(...deployments.map(({ id, cloudProvider, airnodeAddress, stage, airnodeVersion, lastUpdate }) => [
        id,
        (0, cli_1.cloudProviderReadable)(cloudProvider),
        (0, cli_1.airnodeAddressReadable)(airnodeAddress),
        stage,
        airnodeVersion,
        (0, cli_1.timestampReadable)(lastUpdate),
    ]));
    logger.consoleLog(table.toString());
}
exports.listAirnodes = listAirnodes;
async function deploymentInfo(deploymentId) {
    const cloudProviderType = deploymentId.slice(0, 3);
    if (!airnode_node_1.availableCloudProviders.includes(cloudProviderType)) {
        throw new Error(`Invalid deployment ID '${deploymentId}'`);
    }
    const spinner = logger.getSpinner();
    spinner.start(`Fetching info about deployment '${deploymentId}'`);
    if (logger.inDebugMode()) {
        spinner.info();
    }
    const goFetchDeployment = await (0, promise_utils_1.go)(() => fetchDeployment(cloudProviderType, deploymentId));
    if (!goFetchDeployment.success) {
        spinner.stop();
        throw goFetchDeployment.error;
    }
    const { deployment } = goFetchDeployment.data;
    const { id, cloudProvider, airnodeAddress, stage, airnodeVersion, lastUpdate, versions } = deployment;
    const sortedVersions = (0, sortBy_1.default)(versions, 'timestamp').reverse();
    const currentVersionId = sortedVersions.find((version) => version.timestamp === lastUpdate).id;
    const table = new cli_table3_1.default({
        head: ['Version ID', 'Deployment time'],
        style: {
            head: ['bold'],
        },
    });
    table.push(...sortedVersions.map(({ id, timestamp }) => [id, (0, cli_1.timestampReadable)(timestamp)]));
    spinner.succeed();
    logger.consoleLog(`Cloud provider: ${(0, cli_1.cloudProviderReadable)(cloudProvider)}`);
    logger.consoleLog(`Airnode address: ${airnodeAddress}`);
    logger.consoleLog(`Stage: ${stage}`);
    logger.consoleLog(`Airnode version: ${airnodeVersion}`);
    logger.consoleLog(`Deployment ID: ${id}`);
    const tableString = table.toString();
    const tableStringWithCurrent = tableString.replace(new RegExp(`(?<=${currentVersionId}.*?)\n`), ' (current)\n');
    logger.consoleLog(tableStringWithCurrent);
}
exports.deploymentInfo = deploymentInfo;
async function fetchFiles(deploymentId, outputDir, versionId) {
    const cloudProviderType = deploymentId.slice(0, 3);
    if (!airnode_node_1.availableCloudProviders.includes(cloudProviderType)) {
        throw new Error(`Invalid deployment ID '${deploymentId}'`);
    }
    const spinner = logger.getSpinner();
    spinner.start(`Fetching files for deployment '${deploymentId}'${versionId ? ` and version '${versionId}'` : ''}'`);
    if (logger.inDebugMode()) {
        spinner.info();
    }
    const goFetchDeployment = await (0, promise_utils_1.go)(() => fetchDeployment(cloudProviderType, deploymentId, versionId));
    if (!goFetchDeployment.success) {
        spinner.stop();
        throw goFetchDeployment.error;
    }
    const { deployment, version } = goFetchDeployment.data;
    const goDownloadDeploymentFiles = await (0, promise_utils_1.go)(() => downloadDeploymentFiles(cloudProviderType, deployment, version));
    if (!goDownloadDeploymentFiles.success) {
        spinner.stop();
        throw goDownloadDeploymentFiles.error;
    }
    const { configContent, secretsContent } = goDownloadDeploymentFiles.data;
    const goOutputWritable = (0, promise_utils_1.goSync)(() => fs.accessSync(outputDir, fs.constants.W_OK));
    if (!goOutputWritable.success) {
        spinner.stop();
        throw new Error(`Can't write into an output directory '${outputDir}': ${goOutputWritable.error}`);
    }
    const zip = new adm_zip_1.default();
    zip.addFile('config.json', Buffer.from(configContent, 'utf8'));
    zip.addFile('secrets.env', Buffer.from(secretsContent, 'utf8'));
    const outputFile = path.join(outputDir, `${deploymentId}-${version.id}.zip`);
    const goWriteZip = await (0, promise_utils_1.go)(() => zip.writeZipPromise(outputFile));
    if (!goWriteZip.success) {
        spinner.stop();
        throw new Error(`Can't create a zip file '${outputFile}': ${goWriteZip.error}`);
    }
    spinner.succeed(`Files successfully downloaded as '${outputFile}'`);
}
exports.fetchFiles = fetchFiles;
async function saveDeploymentFiles(deploymentId, versionId, configPath, secretsPath) {
    const cloudProviderType = deploymentId.slice(0, 3);
    if (!airnode_node_1.availableCloudProviders.includes(cloudProviderType)) {
        throw new Error(`Invalid deployment ID '${deploymentId}'`);
    }
    const spinner = logger.getSpinner();
    const goFetchDeployment = await (0, promise_utils_1.go)(() => fetchDeployment(cloudProviderType, deploymentId, versionId));
    if (!goFetchDeployment.success) {
        spinner.stop();
        throw goFetchDeployment.error;
    }
    const { deployment, version } = goFetchDeployment.data;
    const sortedVersions = (0, sortBy_1.default)(deployment.versions, 'timestamp').reverse();
    const currentVersionId = sortedVersions.find((version) => version.timestamp === deployment.lastUpdate).id;
    if (deployment.id === deploymentId && currentVersionId === versionId) {
        spinner.stop();
        throw new Error(`Already on version '${versionId}' of deployment '${deploymentId}', can't rollback to the current version`);
    }
    const goDownloadDeploymentFiles = await (0, promise_utils_1.go)(() => downloadDeploymentFiles(cloudProviderType, deployment, version));
    if (!goDownloadDeploymentFiles.success) {
        spinner.stop();
        throw goDownloadDeploymentFiles.error;
    }
    const { configContent, secretsContent } = goDownloadDeploymentFiles.data;
    fs.writeFileSync(configPath, configContent);
    fs.writeFileSync(secretsPath, secretsContent);
}
exports.saveDeploymentFiles = saveDeploymentFiles;
//# sourceMappingURL=index.js.map