/**
 * 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 { EKS } from "aws-sdk" ; import { ZonedDateTime , DateTimeFormatter } from "@js-joda/core" ; import yesno from "yesno" ; import { KubeConfig } from "@kubernetes/client-node" ; import { generateKubeconfigStringForEksCluster } from "@opstrace/aws" ; import { getGcpProjectId , getGKEKubeconfig } from "@opstrace/gcp" ; import { log , hasUpperCase , die , sleep , ExitError } from "@opstrace/utils" ; import { getValidatedGCPAuthOptionsFromFile , GCPAuthOptions } from "@opstrace/gcp" ; import { CLUSTER_NAME_REGEX , KNOWN_AWS_REGIONS } from "@opstrace/config" ; import * as cli from "./index" ; import * as list from "./list" ; /**
 * Validate command line-provided cluster name, with a focus on good error
 * messages.
 */ export function validateClusterNameOrDie ( cn ) { if ( cn . length > 23 ) { die ( `cluster name must not be longer than 23 characters (is: ${ cn . length })` ) ; } if ( cn . length < 2 ) { die ( `cluster name must be at least 2 characters long` ) ; } if ( hasUpperCase ( cn ) ) { die ( `cluster name must not contain uppercase characters` ) ; } if ( ! CLUSTER_NAME_REGEX . test ( cn ) ) { die ( `cluster name does not match regular expression ${ CLUSTER_NAME_REGEX }` ) ; } } /**Sanity-check the contents of GOOGLE_APPLICATION_CREDENTIALS: can the file be
 * read? does it appear to have the right contents? Don't over-do the content
 * validation: source of truth are the GCP libraries which after all re-read
 * the environment variable an do their own file content parsing.
 *
 * Do not handle GOOGLE_APPLICATION_CREDENTIALS to not be set; handled
 * elsewhere.
 */ export function gcpValidateCredFileAndGetDetailOrError ( ) { const fpath = process . env [ "GOOGLE_APPLICATION_CREDENTIALS" ] || "" ; let opts ; try { opts = getValidatedGCPAuthOptionsFromFile ( fpath ) ; } catch ( err ) { die ( `the environment variable GOOGLE_APPLICATION_CREDENTIALS does not appear to point to a valid file ('${ fpath }'): ${ err . message }` ) ; } return opts ; } export async function promptForProceed ( question ? ) { function handleInvalid ( options ) { process . stdout . write ( `\nAnswer ${ options . yesValues . join ( ", " ) } or ${ options . noValues . join ( ", " ) }\n` ) ; } if ( question === undefined ) { question = "Proceed?" ; } if ( cli . CLIARGS . assumeYes !== true ) { const proceed = await yesno ( { question : question + " [y/N]" , invalid : handleInvalid } ) ; log . debug ( "prompt result: %s" , proceed ) ; if ( proceed !== true ) { throw new ExitError ( 1 , "user abort" ) ; } } else { log . info ( "do not prompt: --yes is set" ) ; } } /**
 * Handle error seen by the saga middleware, i.e. when `rootTask()` throws an
 * error. This might very well be an unhandled error from further down in the
 * saga hierarchy. Consider this situation to be fatal. Log any relevant error
 * detail to optimize for debuggability. Also see
 * https://github.com/redux-saga/redux-saga/issues/1698
 * https://redux-saga.js.org/docs/basics/ErrorHandling.html
 */ export function smErrorLastResort ( e , detail ) { if ( e instanceof ExitError ) { if ( e . message !== "" ) { log . error ( e . message ) ; } runtimeShutdown ( e . exitcode ) ; return ; } log . error ( "error seen by saga middleware:\n%s" , e . stack ) ; if ( detail && detail . sagaStack !== undefined ) { log . error ( "saga stack: %s" , detail . sagaStack ) ; } die ( "exit." ) ; } export async function runtimeShutdown ( exitcode ) { log . debug ( "shut down logger, then exit with code %s" , exitcode ) ; log . on ( "finish" , function ( ) { process . exit ( exitcode ) ; } ) ; log . end ( ) ; await sleep ( 2.0 ) ; process . stderr . write ( "winston logging system shutdown timed out, exit anyway (code 2)" ) ; process . exit ( 2 ) ; } export function timestampForFilenames ( ts ) { if ( ts . zone ( ) . toString ( ) !== "Z" ) throw Error ( "code assumes Zulu time" ) ; return ts . format ( DateTimeFormatter . ofPattern ( "yyyyMMdd'-'HHmmss'Z'" ) ) ; } /**
 * Try a dynamic lookup using the provided Opstrace instance name (auto-detect
 * the region). Fall back to other options when that fails:
 * - use what was provided via --region on cmdline
 * - error out if --region was not provided
 */ export async function awsGetClusterRegionWithCmdlineFallback ( ) { if ( cli . CLIARGS . region !== "" ) { log . debug ( "region to destroy in from cli args: %s" , cli . CLIARGS . region ) ; if ( ! KNOWN_AWS_REGIONS . includes ( cli . CLIARGS . region ) ) { const knownRegionString = KNOWN_AWS_REGIONS . join ( ", " ) ; die ( `The provided AWS region (${ cli . CLIARGS . region }) is not ` + `known. Choose one of ${ knownRegionString }.` ) ; } return cli . CLIARGS . region ; } let c ; try { c = await awsGetClusterRegionDynamic ( cli . CLIARGS . instanceName ) ; } catch ( e ) { if ( e instanceof list . ListEksInRegionError ) { die ( "Could not reliably look up the AWS region corresponding to the " + "specified Opstrace instance. Abort operation. " + "If you know what you are doing, you can retry " + "with skipping this region lookup by setting the --region <region> " + "command line parameter." ) ; } throw e ; } if ( c !== undefined ) { return c . awsRegion ; } log . info ( "Scanned all configured AWS regions successfully, but no EKS cluster " + `found for Opstrace instance: ${ cli . CLIARGS . instanceName }` ) ; die ( "Operation aborted: an intact Opstrace instance with the given name " + "was not found across " + "all inspected AWS regions. For cleaning up resource residuals " + "of a previously only partially destroyed Opstrace instance, " + "you can force-run the 'destroy' operation in a specific AWS region: " + "repeat the command and additionally set the --region <region> " + "command line parameter." ) ; } /**
 * This is expected to throw `ListEksInRegionError` when the lookup in a
 * specific region fails.
 *
 */ export async function awsGetClusterRegionDynamic ( lookForOpstraceClusterName ) { log . info ( "starting lookup of matching EKS cluster accross AWS regions" ) ; const ocnRegionMap = { } ; for ( const c of await list . EKSgetOpstraceClustersAcrossManyRegions ( ) ) { ocnRegionMap [ c . opstraceClusterName ] = c ; } if ( lookForOpstraceClusterName in ocnRegionMap ) { const c = ocnRegionMap [ lookForOpstraceClusterName ] ; log . info ( "identified AWS region (found EKS cluster %s): %s" , lookForOpstraceClusterName , c . awsRegion ) ; return c ; } return undefined ; } export async function getKubeConfigForOpstraceClusterOrDie ( cloudProvider , opstraceClusterName ) { let kubeconfig ; if ( cloudProvider == "gcp" ) { gcpValidateCredFileAndGetDetailOrError ( ) ; const pid = await getGcpProjectId ( ) ; log . debug ( "GCP project ID: %s" , pid ) ; const kc = await getGKEKubeconfig ( opstraceClusterName ) ; if ( kc === undefined ) { die ( `error while trying to generate the kubeconfig for cluster ${ opstraceClusterName }` ) ; } kubeconfig = kc ; } else { const c = await awsGetClusterRegionDynamic ( opstraceClusterName ) ; if ( c === undefined ) { die ( `Opstrace cluster not found across all inspected AWS regions: ${ opstraceClusterName }` ) ; } log . info ( "cluster `%s` found in AWS region %s" , c . opstraceClusterName , c . awsRegion ) ; kubeconfig = genKubConfigObjForEKScluster ( c . awsRegion , c . eksCluster ) ; } return kubeconfig ; } function genKubConfigObjForEKScluster ( awsregion , eksCluster ) { log . info ( "generate kubeconfig string for EKS cluster" ) ; const kstring = generateKubeconfigStringForEksCluster ( awsregion , eksCluster ) ; const kubeConfig = new KubeConfig ( ) ; log . info ( "parse kubeconfig string for EKS cluster" ) ; try { kubeConfig . loadFromString ( kstring ) ; } catch ( err ) { log . error ( "bad kconfig string:\n%s" , kstring ) ; die ( `error loading kubeconfig from text: ${ err . code }: ${ err . message }` ) ; } return kubeConfig ; } export function gcpGetClusterRegion ( ) { return "us-west2" ; }