/**
 * Copyright 2020 Opstrace, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ import "source-map-support/register" ; import argparse from "argparse" ; import AWS from "aws-sdk" ; import { ZonedDateTime , ZoneOffset } from "@js-joda/core" ; import { setLogger , buildLogger , log , die , ExitError , ExitSuccess , BUILD_INFO } from "@opstrace/utils" ; import * as aks from "./aks" ; import * as create from "./create" ; import * as destroy from "./destroy" ; import * as list from "./list" ; import * as status from "./status" ; import * as info from "./info" ; import * as upgrade from "./upgrade" ; import * as util from "./util" ; import * as ctoken from "./createTenantAuthToken" ; export { gcpValidateCredFileAndGetDetailOrError } from "./util" ; const DEFAULT_LOG_LEVEL_STDERR = "info" ; const START_TIME_JODA = ZonedDateTime . now ( ZoneOffset . UTC ) ; interface CliOptsInterface { command : string ; cloudProvider : "gcp" | "aws" ; instanceName : string ; clusterConfigFilePath : string ; logLevel : "debug" | "info" | "warning" | "error" ; holdController : boolean ; showVersion : boolean ; region : string ; assumeYes : boolean ; kubeconfigFilePath : string ; tenantApiAuthenticatorKeyFilePath : string ; keyId : string ; tenantName : string ; } export let CLIARGS ; async function main ( ) { parseCmdlineArgs ( ) ; logBuildInfo ( ) ; readCloudCredsOrExit ( ) ; if ( CLIARGS . instanceName !== undefined ) { util . validateClusterNameOrDie ( CLIARGS . instanceName ) ; } switch ( CLIARGS . command ) { case "destroy" : await destroy . destroy ( ) ; break ; case "create" : await create . create ( ) ; break ; case "list" : await list . list ( ) ; break ; case "status" : await status . status ( ) ; break ; case "info" : await info . info ( ) ; break ; case "upgrade" : await upgrade . upgrade ( ) ; break ; case "ta-create-token" : await ctoken . create ( ) ; break ; case "ta-create-keypair" : await aks . createKeypair ( ) ; break ; case "ta-pubkeys-add" : await aks . addKeyToAuthenticatorConfig ( ) ; break ; case "ta-pubkeys-list" : await aks . listKeys ( ) ; break ; case "ta-pubkeys-remove" : await aks . removeKey ( ) ; break ; default : throw Error ( "should never be here" ) ; } throw new ExitSuccess ( ) ; } /**
 * Parse user-given command line arguments.
 *
 * Terminate the process if desired, with the expected output (e.g. in case of
 * --help or --version) or with a decent error message about.
 *
 * Desired side effects:
 *
 *    - Populate `CLIARGS` object.
 *    - Configure logger with desired log level.
 *
 */ function parseCmdlineArgs ( ) { if ( process . argv . length <= 3 && process . argv . includes ( "crashtest" ) ) { throw new Error ( "this is how things look like in view of sadness" ) ; } const mainParser = new argparse . ArgumentParser ( { description : "Opstrace CLI" , prog : "opstrace" } ) ; mainParser . add_argument ( "--version" , { help : "Show version information and exit." , action : "store_true" , default : false , dest : "showVersion" } ) ; const subparsers = mainParser . add_subparsers ( { title : "Available commands" , dest : "command" } ) ; const parserCreate = subparsers . add_parser ( "create" , { help : "Create a new Opstrace instance." } ) ; const parserDestroy = subparsers . add_parser ( "destroy" , { help : "Tear down an existing Opstrace instance." } ) ; const parserList = subparsers . add_parser ( "list" , { help : "List existing Opstrace instances (visible with the configured cloud credentials)." } ) ; const parserStatus = subparsers . add_parser ( "status" , { help : "Check the status of an Opstrace instance (experimental, no promises)." } ) ; const parserInfo = subparsers . add_parser ( "info" , { help : "Get infrastructure and Opstrace instance version information (experimental, no promises)." } ) ; const parserUpgrade = subparsers . add_parser ( "upgrade" , { help : "Upgrade an existing Opstrace instance." } ) ; const parserTACreateKeypair = subparsers . add_parser ( "ta-create-keypair" , { help : "Tenant authentication: create a new RSA key pair." } ) ; const parserTACreateToken = subparsers . add_parser ( "ta-create-token" , { help : "Tenant authentication: create a tenant API authentication token " + "signed with a custom private key. Write token to stdout." , description : "Write token to stdout" } ) ; const parserTAPubKeysAdd = subparsers . add_parser ( "ta-pubkeys-add" , { help : "Tenant authentication: add public key to a running Opstrace instance " + "so that it accepts tokens signed with the corresponding private key." } ) ; const parserTAPubKeysListHelp = "Tenant authentication: list public keys for a running Opstrace instance, " + "i.e. the set of trust anchors for signed authentication tokens." ; const parserTAPubKeysList = subparsers . add_parser ( "ta-pubkeys-list" , { help : parserTAPubKeysListHelp , description : `${ parserTAPubKeysListHelp } The configured public keys are ` + `listed via their key ID on stdout, with one key ID per line.` } ) ; const parserTAPubKeysRemoveHelp = "Tenant authentication: remove a specific public key from the set of trust anchors, " + "i.e. do not accept corresponding authentication tokens anymore." ; const parserTAPubKeysRemove = subparsers . add_parser ( "ta-pubkeys-remove" , { help : parserTAPubKeysRemoveHelp , description : `${ parserTAPubKeysRemoveHelp } The relevant public key needs to be ` + `specified via its key ID. You can list currently configured keys with ` + `the 'ta-pubkeys-list' command.` } ) ; for ( const p of [ parserCreate , parserTACreateKeypair , parserTACreateToken , parserTAPubKeysAdd , parserTAPubKeysList , parserTAPubKeysRemove , parserDestroy , parserList , parserStatus , parserInfo , parserUpgrade , mainParser ] ) { p . add_argument ( "--log-level" , { help : `Set log level for output on stderr. One of: debug, info, warning, error. Default: ${ DEFAULT_LOG_LEVEL_STDERR }` , type : "str" , choices : [ "debug" , "info" , "warning" , "error" ] , default : DEFAULT_LOG_LEVEL_STDERR , metavar : "LEVEL" , dest : "logLevel" } ) ; } for ( const p of [ parserCreate , parserDestroy , parserList , parserStatus , parserInfo , parserUpgrade ] ) { p . add_argument ( "cloudProvider" , { help : "The cloud provider to act on (aws, gcp)." , type : "str" , choices : [ "aws" , "gcp" ] , metavar : "PROVIDER" } ) ; } for ( const p of [ parserCreate , parserDestroy , parserStatus , parserInfo , parserUpgrade ] ) { p . add_argument ( "instanceName" , { help : "The Opstrace instance name ([a-z0-9-_], no more than 23 characters)." , type : "str" , metavar : "INSTANCE_NAME" } ) ; } for ( const p of [ parserCreate , parserStatus , parserUpgrade ] ) { p . add_argument ( "-c" , "--instance-config" , { help : "File path to the config document (YAML). Read from stdin otherwise." , metavar : "CONFIG_FILE_PATH" , dest : "clusterConfigFilePath" , type : "str" , default : "" } ) ; } for ( const p of [ parserCreate , parserDestroy , parserUpgrade ] ) { p . add_argument ( "--yes" , { help : "Automatic yes to prompts; assume 'yes' as answer to all prompts and " + "run non-interactively." , action : "store_true" , default : false , dest : "assumeYes" } ) ; } parserCreate . add_argument ( "--hold-controller" , { help : "Do not deploy controller into k8s cluster (for development purposes)." , action : "store_true" , default : false , dest : "holdController" } ) ; parserCreate . add_argument ( "--write-kubeconfig-file" , { help : "Write kubectl config file (for KUBECONFIG env var) as soon as data is " + "available (right after K8s cluster has been set up)." , type : "str" , metavar : "PATH" , dest : "kubeconfigFilePath" , default : "" } ) ; configureParserTACreateToken ( parserTACreateToken ) ; configureParserTAPubKeysAdd ( parserTAPubKeysAdd ) ; configureParserTAPubKeysList ( parserTAPubKeysList ) ; configureParserTAPubKeysRemove ( parserTAPubKeysRemove ) ; configureParserTACreateKeypair ( parserTACreateKeypair ) ; for ( const p of [ parserDestroy , parserUpgrade ] ) { p . add_argument ( "--region" , { help : "Set the AWS region. Only needed when the automatic " + "region detection fails (when the corresponding EKS cluster " + "cannot be found or inspected). Not yet supported for GCP." , type : "str" , metavar : "REGION" , dest : "region" , default : "" } ) ; } CLIARGS = mainParser . parse_args ( ) ; const logFileName = `opstrace_cli_${ CLIARGS . command }_${ util . timestampForFilenames ( START_TIME_JODA ) }.log` ; setLogger ( buildLogger ( { stderrLevel : CLIARGS . logLevel , filePath : logFileName , fileLevel : "debug" } ) ) ; if ( CLIARGS . showVersion ) { showVersionAndExit ( ) ; } if ( CLIARGS . command === undefined ) { mainParser . print_help ( ) ; throw new ExitError ( 1 , "Missing argument. See message above for instructions." ) ; } log . info ( "logging to file: %s" , logFileName ) ; log . debug ( "cli args:\n%s" , JSON . stringify ( CLIARGS , null , 2 ) ) ; } function configureParserTACreateToken ( parser ) { parser . add_argument ( "instanceName" , { help : "The name of the Opstrace instance to generate the token for. " + "Be sure to set it correctly, otherwise the token will not be accepted." , type : "str" , metavar : "INSTANCE_NAME" } ) ; parser . add_argument ( "tenantName" , { help : "The name of the tenant to generate the token for. " + "Be sure to set it correctly, otherwise the token will not be accepted." , type : "str" , metavar : "TENANT_NAME" } ) ; parser . add_argument ( "tenantApiAuthenticatorKeyFilePath" , { help : "Use the private key encoded in this file to sign the token. " + "The path must point to a PEM RSA private key file using the PKCS#1 or " + "PKCS#8 serialization format." , type : "str" , metavar : "KEYPAIR_FILE_PATH" , default : "" } ) ; } function configureParserTAPubKeysAdd ( parser ) { parser . add_argument ( "cloudProvider" , { help : "The cloud provider to look up the Opstrace instance in (aws, gcp)." , type : "str" , choices : [ "aws" , "gcp" ] , metavar : "PROVIDER" } ) ; parser . add_argument ( "instanceName" , { help : "The name of the Opstrace instance to change the authenticator configuration for." , type : "str" , metavar : "INSTANCE_NAME" } ) ; parser . add_argument ( "tenantApiAuthenticatorKeyFilePath" , { help : "Use the RSA public key encoded in this file. The path must point to " + "a PEM-encoded private or public key file (a private key file encodes " + "the complete key pair, and a public key file encodes just the " + "public key." , type : "str" , metavar : "KEY_FILE_PATH" , default : "" } ) ; } function configureParserTAPubKeysList ( parser ) { parser . add_argument ( "cloudProvider" , { help : "The cloud provider to look up the Opstrace instance in (aws, gcp)." , type : "str" , choices : [ "aws" , "gcp" ] , metavar : "PROVIDER" } ) ; parser . add_argument ( "instanceName" , { help : "The name of the Opstrace instance to look up the authenticator configuration for." , type : "str" , metavar : "INSTANCE_NAME" } ) ; } function configureParserTAPubKeysRemove ( parser ) { parser . add_argument ( "cloudProvider" , { help : "The cloud provider to look up the Opstrace instance in (aws, gcp)." , type : "str" , choices : [ "aws" , "gcp" ] , metavar : "PROVIDER" } ) ; parser . add_argument ( "instanceName" , { help : "The name of the instance to change the authenticator configuration in." , type : "str" , metavar : "INSTANCE_NAME" } ) ; parser . add_argument ( "keyId" , { help : "Use ID of the public key to remove." , type : "str" , metavar : "KEY_ID" , default : "" } ) ; } function configureParserTACreateKeypair ( parser ) { parser . add_argument ( "tenantApiAuthenticatorKeyFilePath" , { help : "Write the RSA private key (also containing the public key) in the " + "PKCS#8 format to this file." , type : "str" , metavar : "KEYPAIR_FILE_PATH" , default : "" } ) ; } /**
 * Read/discover cloud credentials from environment / file system. Error out
 * with a decent error message when a requirement is not met.
 */ function readCloudCredsOrExit ( ) { if ( CLIARGS . cloudProvider == "gcp" ) { if ( ! ( "GOOGLE_APPLICATION_CREDENTIALS" in process . env ) ) { die ( "environment variable GOOGLE_APPLICATION_CREDENTIALS is not set" ) ; } } if ( CLIARGS . cloudProvider == "aws" ) { const creds = AWS . config . credentials ; if ( creds === undefined || creds === null ) { die ( "Could not automatically discover AWS credentials. " + "Consider setting the environment variables " + "AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or ... [link to docs]" ) ; } const accesskeyPfxSfx = `${ creds ?. accessKeyId . slice ( 0 , 4 ) }...${ creds ?. accessKeyId . slice ( - 4 ) }` ; log . info ( "Discovered AWS credentials. Access key: %s" , accesskeyPfxSfx ) ; } } /**
 * Write version information to stdout and exit with status 0, indicating
 * success.
 *
 * Debug-log (to stderr? I hope) more build info.
 */ function showVersionAndExit ( ) { process . stdout . write ( `${ BUILD_INFO . VERSION_STRING }\n` ) ; log . debug ( "BUILD_INFO_COMMIT: %s" , BUILD_INFO . COMMIT ) ; log . debug ( "BUILD_INFO_TIME_RFC3339: %s" , BUILD_INFO . BUILD_TIME_RFC3339 ) ; log . debug ( "BUILD_INFO_HOSTNAME: %s" , BUILD_INFO . BUILD_HOSTNAME ) ; log . debug ( "BUILD_INFO_BRANCH_NAME: %s" , BUILD_INFO . BRANCH_NAME ) ; throw new ExitSuccess ( ) ; } function logBuildInfo ( ) { log . debug ( `CLI build information: ${ JSON . stringify ( BUILD_INFO , null , 2 ) }` ) ; } async function mainWrapper ( ) { try { await main ( ) ; } catch ( e ) { if ( e instanceof ExitSuccess ) { util . runtimeShutdown ( 0 ) ; } if ( e instanceof ExitError ) { if ( e . message !== "" ) { log . error ( e . message ) ; } util . runtimeShutdown ( e . exitcode ) ; } throw e ; } } if ( require . main === module ) { process . on ( "SIGINT" , function ( ) { log . debug ( "Received SIGINT, exiting" ) ; util . runtimeShutdown ( 1 ) ; } ) ; process . on ( "unhandledRejection" , err => { throw err ; } ) ; mainWrapper ( ) ; }