@coalescesoftware/coa 1.0.121 → 1.0.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coalescesoftware/coa",
3
- "version": "1.0.121",
3
+ "version": "1.0.122",
4
4
  "license": "ISC",
5
5
  "author": "Coalesce Automation, Inc.",
6
6
  "main": "index.js",
@@ -0,0 +1,170 @@
1
+ /* eslint-disable multiline-comment-style */
2
+ import * as Shared from '@coalescesoftware/shared'
3
+ import * as immer from 'immer'
4
+ const os = require('os')
5
+ const path = require('path');
6
+ const ini = require('ini')
7
+ const fs = require('fs')
8
+
9
+ const RemoveCredentialsFromCLIProfile = (cliProfile:Partial<ICLIProfile>)=> {
10
+ return immer.produce(cliProfile, (draft)=> {
11
+ if (draft.snowflakePassword)
12
+ draft.snowflakePassword = '<REDACTED>'
13
+
14
+ if (draft.snowflakeAccount)
15
+ draft.snowflakeAccount = '<REDACTED>'
16
+ })
17
+ }
18
+
19
+ const RemoveCredentialsFromCLIProfiles = (cliProfiles:ICLIProfilesPartial)=> {
20
+ return immer.produce(cliProfiles, (draftState) => {
21
+ Object.keys(draftState).forEach((cliProfileName:string)=> {
22
+ draftState[cliProfileName] = RemoveCredentialsFromCLIProfile(cliProfiles[cliProfileName])
23
+ })
24
+ })
25
+ }
26
+
27
+ const CLILogger = Shared.Logging.GetLogger(Shared.Logging.LoggingArea.CLI_INTERNAL)
28
+ export interface ICLIConfig {
29
+ token: string
30
+ runDetails: {
31
+ environmentID:string,
32
+ jobID?: number,
33
+ includeNodesSelector?: string,
34
+ excludeNodesSelector?: string,
35
+ }
36
+ userCredentials: Shared.Runner.ISnowflakeUserCredentials,
37
+ runtimeParameters:Shared.Runner.TRunTimeParametersStringType
38
+ }
39
+
40
+ export const GetDefaultLocationForCoaConfigFile = ():string=> {
41
+ const homedir = os.homedir()
42
+ const coaConfigLocation = path.join(homedir, ".coa/config")
43
+ CLILogger.info("using default location", coaConfigLocation)
44
+ return coaConfigLocation
45
+ }
46
+
47
+ //voodoo typescript magic in order to get runtime and compiletime ICLIProfile code
48
+ //https://stackoverflow.com/questions/45670705/iterate-over-interface-properties-in-typescript
49
+ export const ICLIProfileExample= {
50
+ profile:"Profile To Use",
51
+ environmentID:"Environment ID",
52
+ parameters: "Parameters",
53
+ snowflakeAccount: "Snowflake Account To Use",
54
+ snowflakeAuthType:"Snowflake Auth Type (Basic, KeyPair)",
55
+ snowflakeKeyPairPath: "Snowflake Key Pair Path",
56
+ snowflakePassword:"Snowflake Password",
57
+ snowflakeRole:"Snowflake Role",
58
+ snowflakeUsername:"Snowflake Username",
59
+ snowflakeWarehouse: "Snowflake Warehouse",
60
+ token: "Coalesce Token",
61
+ jobID: "Coalesce JobID",
62
+ include: "Coalesce Node Selector",
63
+ exclude: "Coalesce Node Selector"
64
+ }
65
+
66
+
67
+ export type ICLIProfile = typeof ICLIProfileExample
68
+
69
+ type ICLIProfiles = {[cliProfileName:string]: ICLIProfile}
70
+ type ICLIProfilesPartial = {[cliProfileName:string]: Partial<ICLIProfile>}
71
+
72
+ const ReadCLIProfiles = (filePath: string | null): Promise<ICLIProfiles> => {
73
+ let filePathToUse: string | null
74
+
75
+ if (!filePath) {
76
+ filePathToUse = GetDefaultLocationForCoaConfigFile()
77
+ } else {
78
+ filePathToUse = filePath
79
+ }
80
+
81
+ return fs.promises.readFile(filePathToUse, 'utf-8')
82
+ .then((file)=> {
83
+ return ini.parse(file)
84
+ })
85
+ .catch((err)=> {
86
+ CLILogger.error("unable to read cli profile file filePath:", filePath, err)
87
+ if (!filePath) //if no filepath was specified, silently proceed
88
+ return {}
89
+ else //unable to proceed couldnt read file
90
+ throw new Error(`unable to read cli profile:${filePathToUse}`)
91
+ })
92
+ }
93
+ const GetFinalCLIProfile = (commandLineOverrides,
94
+ configFileLocation:string|null
95
+ ):Promise<Partial<ICLIProfile>>=> {
96
+ let profileToUseOverride = commandLineOverrides.profile ? commandLineOverrides.profile: null
97
+ return ReadCLIProfiles(configFileLocation)
98
+ .then((cliProfiles:ICLIProfiles)=> {
99
+ const defaultCLIProfile = cliProfiles.default
100
+ let finalCLIProfile = {}
101
+
102
+ //if theres a default cli profile, start with that
103
+ if (defaultCLIProfile) {
104
+ finalCLIProfile = defaultCLIProfile
105
+ }
106
+
107
+ //if a profile has been specified, use that
108
+ const profileToUse: string = profileToUseOverride || //cli override
109
+ (!!defaultCLIProfile && defaultCLIProfile.profile) //default profile exists
110
+ ||""
111
+
112
+ CLILogger.info("using profile", profileToUse, "cliProfiles", JSON.stringify(RemoveCredentialsFromCLIProfiles(cliProfiles)))
113
+ if (profileToUse) {
114
+ if (!(profileToUse in cliProfiles)) {
115
+ throw new Error(`unable to find profile ${profileToUse}`)
116
+ }
117
+ const coaConfigProfile: ICLIProfile = cliProfiles[profileToUse]
118
+ finalCLIProfile = { ...finalCLIProfile, ...coaConfigProfile }
119
+ }
120
+ finalCLIProfile = { ...finalCLIProfile, ...commandLineOverrides }
121
+ return finalCLIProfile
122
+ })
123
+ }
124
+
125
+
126
+ //Get CLI Config given command line overrides and a config file
127
+ //config file location can be null - which means to use default location
128
+ export const GetCLIConfig = (commandLineOverrides:Partial<ICLIProfile>, configFileLocation:string|null): Promise<ICLIConfig> => {
129
+ return GetFinalCLIProfile(commandLineOverrides,
130
+ configFileLocation
131
+ ).then((cliProfile: Partial<ICLIProfile>) => {
132
+ CLILogger.info("got final cli profile configFileLocation:", configFileLocation, JSON.stringify(RemoveCredentialsFromCLIProfile(cliProfile)))
133
+ const cliConfig = {
134
+ token: cliProfile.token,
135
+ runDetails: {
136
+ environmentID: cliProfile.environmentID,
137
+ jobID: cliProfile.jobID,
138
+ includeNodesSelector: cliProfile.include,
139
+ excludeNodesSelector: cliProfile.exclude
140
+ },
141
+ userCredentials: {
142
+ snowflakeAccount: cliProfile.snowflakeAccount,
143
+ snowflakeAuthType: cliProfile.snowflakeAuthType,
144
+ snowflakePassword: cliProfile.snowflakePassword,
145
+ snowflakeKeyPairPath: cliProfile.snowflakeKeyPairPath,
146
+ snowflakeRole: cliProfile.snowflakeRole,
147
+ snowflakeUsername: cliProfile.snowflakeUsername,
148
+ snowflakeWarehouse:cliProfile.snowflakeWarehouse
149
+ },
150
+ runtimeParameters: cliProfile.parameters
151
+ }
152
+ Shared.Common.CleanupUndefinedValuesFromObject(cliConfig)
153
+ return cliConfig as ICLIConfig
154
+ })
155
+ }
156
+
157
+ // TODO: create a profile class that takes the cmd object in the constructor
158
+ // store contents of configuration file
159
+ // if no config file path option
160
+ // check if there's a valid configuration file in the current working directory or home directory
161
+ // if none exists
162
+ // create a configuration file with empty default profile
163
+ // prompt user to either rerun the command with valid config file, per-command flags for all profile values, or run `coa init` to go through prompts for creating a profile
164
+ // if a configuration file exists, but any required property holds a falsy value
165
+ // inform user which fields are missing and prompt to run `coa init` to fill out invalid fields
166
+ // method: validate configuration file
167
+ // method: get final profile for current command
168
+
169
+ // create a function getCLIProfile that returns a CLI profile, this should return a promise that resolve in CLI profile
170
+ // hardcode a profile
@@ -0,0 +1,130 @@
1
+ import * as Shared from "@coalescesoftware/shared"
2
+ import * as fs from "fs";
3
+ import * as crypto from "crypto"
4
+ import * as RunOutput from './RunOutput'
5
+
6
+ const RunnerBackendLogger = Shared.Logging.GetLogger(Shared.Logging.LoggingArea.RunnerBackend)
7
+ const LogCLIInternal = Shared.Logging.GetLogger(Shared.Logging.LoggingArea.CLI_INTERNAL)
8
+
9
+ export const CleanupCLIJob = (runCompletion: Promise<any>, firebase: any, logContext: Shared.Logging.LogContext, action: string) => {
10
+ const cleanupPromise = new Promise<void>((resolve, reject) => {
11
+ let promiseError = null
12
+ runCompletion
13
+ .catch((error) => {
14
+ promiseError = error
15
+ })
16
+ .finally(() => {
17
+ try {
18
+ if (firebase) {
19
+ Shared.Common.DeleteFirebaseInstance(firebase)
20
+ }
21
+
22
+ } catch (error) {
23
+ RunnerBackendLogger.errorContext(
24
+ logContext,
25
+ `Error during ${action}WithCLI Finally cleanup: `,
26
+ error,
27
+ )
28
+ }
29
+
30
+ if (promiseError) {
31
+ reject(promiseError)
32
+ } else {
33
+ resolve()
34
+ }
35
+ })
36
+ })
37
+
38
+ return cleanupPromise
39
+ }
40
+
41
+ const GetKeyPairKey = (keyPairPath: string): Promise<Buffer | null> => {
42
+ return new Promise((resolve, reject) => {
43
+ Shared.Common.assert(Shared.Logging.LoggingArea.RunnerBackend, !!keyPairPath, "ERROR (GetKeyPairPath): invalid or missing keyPairPath")
44
+ const privateKeyFile = fs.readFileSync(keyPairPath);
45
+ const privateKeyObject = crypto.createPrivateKey({
46
+ key: privateKeyFile,
47
+ format: "pem",
48
+ passphrase: "passphrase"
49
+ });
50
+ const privateKey = privateKeyObject.export({
51
+ format: "pem",
52
+ type: "pkcs8"
53
+ });
54
+ if (!privateKey) {
55
+ reject(new Error("ERROR (GetKeyPairPath): invalid or missing privateKey"))
56
+ } else {
57
+ resolve(privateKey as Buffer)
58
+ }
59
+ })
60
+ }
61
+
62
+ export const GetUserConnectionForCLI = (
63
+ userID: string,
64
+ runInfo: Shared.Runner.IRunInfo
65
+ ): Promise<Shared.ConnectionOperations.IUserConnection> => {
66
+ return new Promise((resolve, reject) => {
67
+ const output: Shared.ConnectionOperations.IUserConnection = {
68
+ connectionDetails: {
69
+ userID,
70
+ user: runInfo.userCredentials?.snowflakeUsername!,
71
+ role: runInfo.userCredentials?.snowflakeRole!,
72
+ warehouse: runInfo.userCredentials?.snowflakeWarehouse!,
73
+ },
74
+ connectionType: runInfo.userCredentials?.snowflakeAuthType!
75
+ }
76
+
77
+ if (!runInfo.userCredentials?.snowflakeAuthType) {
78
+ reject(new Error("ERROR (GetUserConnectionForCLI): no auth type provided"))
79
+ } else if (runInfo.userCredentials?.snowflakeAuthType! === Shared.ConnectionOperations.EUserConnectionTypes.keyPair) {
80
+ GetKeyPairKey(Shared.Common.getValueSafe(runInfo, [ "userCredentials", "snowflakeKeyPairPath" ], ""))
81
+ .then((keyPair) => {
82
+ output.connectionDetails.keyPair = keyPair
83
+ resolve(output)
84
+ })
85
+ .catch((err) => {
86
+ reject(err)
87
+ })
88
+ } else {
89
+ resolve(output)
90
+ }
91
+ })
92
+ }
93
+
94
+ ////////
95
+ // Runtime Parameters
96
+ ///////
97
+ export const GetRuntimeParametersFromFirestore = (
98
+ firestore: any,
99
+ teamID: string,
100
+ environmentID: number,
101
+ ): Promise<Shared.Runner.TRunTimeParametersStringType> => {
102
+ return Shared.CommonOperations.getWorkspaceDocumentRefAdmin(
103
+ firestore,
104
+ teamID,
105
+ environmentID
106
+ ).get().then((workspace) => {
107
+ return workspace.get("runTimeParameters")
108
+ })
109
+ }
110
+
111
+ export const ValidateRuntimeParameters = (runtimeParameters) => {
112
+ try {
113
+ JSON.parse(runtimeParameters)
114
+ } catch (error) {
115
+ throw new Error(`Failed to parse runtime parameters: ${(error as any).message}`)
116
+ }
117
+ }
118
+
119
+ ////////
120
+ // Output File
121
+ ///////
122
+ export const FinishWithOutputFile = (logContextToUse: Shared.Logging.LogContext, outputFilePath: string | null, runCounter: number, token: string): Promise<void> => {
123
+ if (!outputFilePath) {
124
+ return Promise.resolve()
125
+ }
126
+ else {
127
+ LogCLIInternal.infoContext(logContextToUse, "saving run results to file", outputFilePath)
128
+ return RunOutput.SaveRunOutputToFile(outputFilePath, runCounter.toString(), token)
129
+ }
130
+ }
package/src/Deploy.ts ADDED
@@ -0,0 +1,84 @@
1
+ import * as Shared from "@coalescesoftware/shared"
2
+ import * as CommonCLI from "./CommonCLI"
3
+ const CryptoJS = require("crypto-js")
4
+ const v8 = require("v8")
5
+
6
+ Shared.Snowflake.CryptoJS = CryptoJS
7
+ Shared.Snowflake.salt = CryptoJS.lib.WordArray.random(128 / 8);
8
+ Shared.Snowflake.v8 = v8;
9
+
10
+ const LogCLI = Shared.Logging.GetLogger(Shared.Logging.LoggingArea.CLI)
11
+
12
+ /**
13
+ *
14
+ * @param plan
15
+ * @param config
16
+ * @param token
17
+ * @returns
18
+ */
19
+ export const DeployWithCLI = (
20
+ plan: Shared.DeployOperations.IPlan,
21
+ config: Shared.Runner.IRunInfo,
22
+ token: string,
23
+ ): Promise<Shared.SchedulerOperations.IRunCounterAndRunCompletion> => {
24
+ let firebase: any
25
+ let logContext: Shared.Logging.LogContext
26
+ let teamDetailsStored: Shared.ConnectionOperations.ITeamInfoAndFirebase
27
+ let RunSQL: Shared.RunStepHelpers.TRunSQL
28
+ let runInfo
29
+
30
+ return Shared.SchedulerOperations.AuthenticateFirebaseTokenAndRetrieveTeamInfoForCLI(
31
+ token,
32
+ undefined,
33
+ ).then((teamDetails: Shared.ConnectionOperations.ITeamInfoAndFirebase) => {
34
+ teamDetailsStored = teamDetails
35
+ const { teamInfo, teamInfo: { fbUserID: userID, fbTeamID: teamID } } = teamDetails
36
+ firebase = teamDetails.firebase
37
+ const environmentID: number = +config.runDetails?.environmentID!
38
+ logContext = Shared.Logging.CreateLogContext(teamID, environmentID, userID)
39
+
40
+ const connectionCache: Shared.Snowflake.ConnectionStorageClass = new Shared.Snowflake.ConnectionStorageClass()
41
+ RunSQL = Shared.SQLExecutorCreators.CreateRunSQLWithoutScheduler(
42
+ teamDetails,
43
+ connectionCache,
44
+ )
45
+
46
+ runInfo = Shared.DeployOperations.CreateDeployRequestObject(
47
+ plan.plan!,
48
+ plan.environmentState!,
49
+ teamInfo,
50
+ plan.gitInfo!,
51
+ plan.targetEnvironment,
52
+ plan.runtimeParameters,
53
+ )
54
+
55
+ runInfo.userCredentials = config.userCredentials
56
+ return CommonCLI.GetUserConnectionForCLI(
57
+ userID,
58
+ runInfo
59
+ )
60
+ })
61
+ .then((connection: Shared.ConnectionOperations.IUserConnection) => {
62
+ LogCLI.infoContext(logContext, "Deploy starting (CLI)")
63
+ return Shared.SchedulerOperations.BECLI_HandleDeploy(
64
+ runInfo,
65
+ teamDetailsStored,
66
+ RunSQL,
67
+ connection,
68
+ )
69
+ })
70
+ .then(({ runCounter, runCompletion }: Shared.SchedulerOperations.IRunCounterAndRunCompletion) => {
71
+ const cleanupPromise: Promise<void> = CommonCLI.CleanupCLIJob(
72
+ runCompletion,
73
+ firebase,
74
+ logContext,
75
+ "Deploy"
76
+ )
77
+
78
+ return {
79
+ runCounter,
80
+ runCompletion: cleanupPromise,
81
+ logContext,
82
+ }
83
+ })
84
+ }