@coalescesoftware/coa 1.0.155 → 1.0.156

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.155",
3
+ "version": "1.0.156",
4
4
  "license": "ISC",
5
5
  "author": "Coalesce Automation, Inc.",
6
6
  "main": "index.js",
@@ -0,0 +1,172 @@
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
+ snowflakeKeyPairPass: "Snowflake Key Pair Pass",
57
+ snowflakePassword:"Snowflake Password",
58
+ snowflakeRole:"Snowflake Role",
59
+ snowflakeUsername:"Snowflake Username",
60
+ snowflakeWarehouse: "Snowflake Warehouse",
61
+ token: "Coalesce Token",
62
+ jobID: "Coalesce JobID",
63
+ include: "Coalesce Node Selector",
64
+ exclude: "Coalesce Node Selector"
65
+ }
66
+
67
+
68
+ export type ICLIProfile = typeof ICLIProfileExample
69
+
70
+ type ICLIProfiles = {[cliProfileName:string]: ICLIProfile}
71
+ type ICLIProfilesPartial = {[cliProfileName:string]: Partial<ICLIProfile>}
72
+
73
+ const ReadCLIProfiles = (filePath: string | null): Promise<ICLIProfiles> => {
74
+ let filePathToUse: string | null
75
+
76
+ if (!filePath) {
77
+ filePathToUse = GetDefaultLocationForCoaConfigFile()
78
+ } else {
79
+ filePathToUse = filePath
80
+ }
81
+
82
+ return fs.promises.readFile(filePathToUse, 'utf-8')
83
+ .then((file)=> {
84
+ return ini.parse(file)
85
+ })
86
+ .catch((err)=> {
87
+ CLILogger.error("unable to read cli profile file filePath:", filePath, err)
88
+ if (!filePath) //if no filepath was specified, silently proceed
89
+ return {}
90
+ else //unable to proceed couldnt read file
91
+ throw new Error(`unable to read cli profile:${filePathToUse}`)
92
+ })
93
+ }
94
+ const GetFinalCLIProfile = (commandLineOverrides,
95
+ configFileLocation:string|null
96
+ ):Promise<Partial<ICLIProfile>>=> {
97
+ let profileToUseOverride = commandLineOverrides.profile ? commandLineOverrides.profile: null
98
+ return ReadCLIProfiles(configFileLocation)
99
+ .then((cliProfiles:ICLIProfiles)=> {
100
+ const defaultCLIProfile = cliProfiles.default
101
+ let finalCLIProfile = {}
102
+
103
+ //if theres a default cli profile, start with that
104
+ if (defaultCLIProfile) {
105
+ finalCLIProfile = defaultCLIProfile
106
+ }
107
+
108
+ //if a profile has been specified, use that
109
+ const profileToUse: string = profileToUseOverride || //cli override
110
+ (!!defaultCLIProfile && defaultCLIProfile.profile) //default profile exists
111
+ ||""
112
+
113
+ CLILogger.info("using profile", profileToUse, "cliProfiles", JSON.stringify(RemoveCredentialsFromCLIProfiles(cliProfiles)))
114
+ if (profileToUse) {
115
+ if (!(profileToUse in cliProfiles)) {
116
+ throw new Error(`unable to find profile ${profileToUse}`)
117
+ }
118
+ const coaConfigProfile: ICLIProfile = cliProfiles[profileToUse]
119
+ finalCLIProfile = { ...finalCLIProfile, ...coaConfigProfile }
120
+ }
121
+ finalCLIProfile = { ...finalCLIProfile, ...commandLineOverrides }
122
+ return finalCLIProfile
123
+ })
124
+ }
125
+
126
+
127
+ //Get CLI Config given command line overrides and a config file
128
+ //config file location can be null - which means to use default location
129
+ export const GetCLIConfig = (commandLineOverrides:Partial<ICLIProfile>, configFileLocation:string|null): Promise<ICLIConfig> => {
130
+ return GetFinalCLIProfile(commandLineOverrides,
131
+ configFileLocation
132
+ ).then((cliProfile: Partial<ICLIProfile>) => {
133
+ CLILogger.info("got final cli profile configFileLocation:", configFileLocation, JSON.stringify(RemoveCredentialsFromCLIProfile(cliProfile)))
134
+ const cliConfig = {
135
+ token: cliProfile.token,
136
+ runDetails: {
137
+ environmentID: cliProfile.environmentID,
138
+ jobID: cliProfile.jobID,
139
+ includeNodesSelector: cliProfile.include,
140
+ excludeNodesSelector: cliProfile.exclude
141
+ },
142
+ userCredentials: {
143
+ snowflakeAccount: cliProfile.snowflakeAccount,
144
+ snowflakeAuthType: cliProfile.snowflakeAuthType,
145
+ snowflakePassword: cliProfile.snowflakePassword,
146
+ snowflakeKeyPairPath: cliProfile.snowflakeKeyPairPath,
147
+ snowflakeKeyPairPass: cliProfile.snowflakeKeyPairPass,
148
+ snowflakeRole: cliProfile.snowflakeRole,
149
+ snowflakeUsername: cliProfile.snowflakeUsername,
150
+ snowflakeWarehouse:cliProfile.snowflakeWarehouse
151
+ },
152
+ runtimeParameters: cliProfile.parameters
153
+ }
154
+ Shared.Common.CleanupUndefinedValuesFromObject(cliConfig)
155
+ return cliConfig as ICLIConfig
156
+ })
157
+ }
158
+
159
+ // TODO: create a profile class that takes the cmd object in the constructor
160
+ // store contents of configuration file
161
+ // if no config file path option
162
+ // check if there's a valid configuration file in the current working directory or home directory
163
+ // if none exists
164
+ // create a configuration file with empty default profile
165
+ // 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
166
+ // if a configuration file exists, but any required property holds a falsy value
167
+ // inform user which fields are missing and prompt to run `coa init` to fill out invalid fields
168
+ // method: validate configuration file
169
+ // method: get final profile for current command
170
+
171
+ // create a function getCLIProfile that returns a CLI profile, this should return a promise that resolve in CLI profile
172
+ // hardcode a profile
@@ -0,0 +1,133 @@
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, keyPairPass: 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: keyPairPass
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(
81
+ Shared.Common.getValueSafe(runInfo, [ "userCredentials", "snowflakeKeyPairPath" ], ""),
82
+ Shared.Common.getValueSafe(runInfo, [ "userCredentials", "snowflakeKeyPairPass" ], "")
83
+ )
84
+ .then((keyPair) => {
85
+ output.connectionDetails.keyPair = keyPair
86
+ resolve(output)
87
+ })
88
+ .catch((err) => {
89
+ reject(err)
90
+ })
91
+ } else {
92
+ resolve(output)
93
+ }
94
+ })
95
+ }
96
+
97
+ ////////
98
+ // Runtime Parameters
99
+ ///////
100
+ export const GetRuntimeParametersFromFirestore = (
101
+ firestore: any,
102
+ teamID: string,
103
+ environmentID: number,
104
+ ): Promise<Shared.Runner.TRunTimeParametersStringType> => {
105
+ return Shared.CommonOperations.getWorkspaceDocumentRefAdmin(
106
+ firestore,
107
+ teamID,
108
+ environmentID
109
+ ).get().then((workspace) => {
110
+ return workspace.get("runTimeParameters")
111
+ })
112
+ }
113
+
114
+ export const ValidateRuntimeParameters = (runtimeParameters) => {
115
+ try {
116
+ JSON.parse(runtimeParameters)
117
+ } catch (error) {
118
+ throw new Error(`Failed to parse runtime parameters: ${(error as any).message}`)
119
+ }
120
+ }
121
+
122
+ ////////
123
+ // Output File
124
+ ///////
125
+ export const FinishWithOutputFile = (logContextToUse: Shared.Logging.LogContext, outputFilePath: string | null, runCounter: number, token: string): Promise<void> => {
126
+ if (!outputFilePath) {
127
+ return Promise.resolve()
128
+ }
129
+ else {
130
+ LogCLIInternal.infoContext(logContextToUse, "saving run results to file", outputFilePath)
131
+ return RunOutput.SaveRunOutputToFile(outputFilePath, runCounter.toString(), token)
132
+ }
133
+ }
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
+ }