From 19f68774c8f8a03a08e92e3ad6ac3f4392f0a78f Mon Sep 17 00:00:00 2001 From: Joseph Soto Date: Mon, 8 Jul 2024 13:55:45 -0700 Subject: [PATCH] Add k8s-env --dev option (#3667) This PR makes the following changes: - Adds `--dev` option to k8s-env command - This flag when enabled will add the entire `teraslice` repo as a volume to allow for fast build changes when running in kubernetes. - Adds dev compatibility to k8s master backend - Fixes asset-storage setting in k8s-env - This resolves a bug where launching k8s-env with `asset-storage` set to `s3` would not work correctly --- Dockerfile.dev | 19 ++++ e2e/k8s/kindConfigDefaultPortsDev.yaml | 45 ++++++++ package.json | 2 + packages/scripts/package.json | 2 +- packages/scripts/src/cmds/k8s-env.ts | 9 +- packages/scripts/src/helpers/interfaces.ts | 7 ++ packages/scripts/src/helpers/k8s-env/index.ts | 24 +++- .../scripts/src/helpers/k8s-env/interfaces.ts | 1 + packages/scripts/src/helpers/k8s-env/k8s.ts | 37 ++++++- packages/scripts/src/helpers/kind.ts | 103 +++++++++++++++++- .../scripts/src/helpers/publish/interfaces.ts | 1 + packages/scripts/src/helpers/publish/utils.ts | 2 +- packages/scripts/src/helpers/scripts.ts | 6 +- .../backends/kubernetes/k8sResource.ts | 17 +++ yarn.lock | 95 ++++++++++++++-- 15 files changed, 346 insertions(+), 24 deletions(-) create mode 100644 Dockerfile.dev create mode 100644 e2e/k8s/kindConfigDefaultPortsDev.yaml diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 00000000000..413d5a37c31 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,19 @@ +# NODE_VERSION is set by default in the config.ts, the following value will only +# be used if you build images by default with docker build +ARG NODE_VERSION=18.19.1 +FROM terascope/node-base:${NODE_VERSION} + +ENV NODE_ENV production + +ENV YARN_SETUP_ARGS "--prod=false --silent --frozen-lockfile" + +# Check to see if distutils is installed because python 3.12 removed it +RUN python3 -c "import distutils" || (apk update && apk add py3-setuptools) + +EXPOSE 5678 + +# set up the volumes +VOLUME /app/config /app/logs /app/assets +ENV TERAFOUNDATION_CONFIG /app/config/teraslice.yaml + +CMD ["yarn", "start:nodemon"] diff --git a/e2e/k8s/kindConfigDefaultPortsDev.yaml b/e2e/k8s/kindConfigDefaultPortsDev.yaml new file mode 100644 index 00000000000..943e90c54a0 --- /dev/null +++ b/e2e/k8s/kindConfigDefaultPortsDev.yaml @@ -0,0 +1,45 @@ +# +# This config file is used in the case that "--dev" is enabled in k8s-env +# which will mount the host machines teraslice repo to the k8s resources +# +kind: Cluster +name: k8s-env +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: +- role: control-plane + extraPortMappings: + - containerPort: 30200 # Map internal elasticsearch service to host port + hostPort: 9200 + - containerPort: 30678 # Map internal teraslice service to host port + hostPort: 5678 + - containerPort: 30092 # Map internal kafka service to host port + hostPort: 9092 + - containerPort: 30900 # Map internal minio service to host port + hostPort: 9000 + extraMounts: + - hostPath: ./e2e/autoload + containerPath: /autoload + ### All extra mounts that the Dockerfile.dev image needs to run teraslice + - hostPath: ./packages + containerPath: /packages + - hostPath: ./scripts + containerPath: /scripts + - hostPath: ./types + containerPath: /types + - hostPath: ./.yarn + containerPath: /.yarn + - hostPath: ./.yarnclean.ci + containerPath: /.yarnclean.ci + - hostPath: ./package.json + containerPath: /package.json + - hostPath: ./yarn.lock + containerPath: /yarn.lock + - hostPath: ./tsconfig.json + containerPath: /tsconfig.json + - hostPath: ./.yarnrc + containerPath: /.yarnrc + - hostPath: ./service.js + containerPath: /service.js + - hostPath: ./node_modules + containerPath: /node_modules + ### diff --git a/package.json b/package.json index 8c1aecafb8f..0844283d2f9 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "setup": "yarn $YARN_SETUP_ARGS && yarn run build --force", "start": "node service.js", "start:dev": "yarn build && env NODE_ENV=development node service.js", + "start:nodemon": "nodemon --exitcrash service.js", "sync": "ts-scripts sync", "test": "ts-scripts test" }, @@ -57,6 +58,7 @@ "jest-extended": "^3.2.4", "jest-watch-typeahead": "^2.2.2", "node-notifier": "^10.0.1", + "nodemon": "^3.1.4", "ts-jest": "^29.1.4", "typescript": "~5.2.2" }, diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 8974c164b21..95c2cbea4c6 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,7 +1,7 @@ { "name": "@terascope/scripts", "displayName": "Scripts", - "version": "0.78.0", + "version": "0.79.0", "description": "A collection of terascope monorepo scripts", "homepage": "https://github.com/terascope/teraslice/tree/master/packages/scripts#readme", "bugs": { diff --git a/packages/scripts/src/cmds/k8s-env.ts b/packages/scripts/src/cmds/k8s-env.ts index 3b62d13dc6a..b34420b51d7 100644 --- a/packages/scripts/src/cmds/k8s-env.ts +++ b/packages/scripts/src/cmds/k8s-env.ts @@ -10,6 +10,7 @@ const cmd: CommandModule = { builder(yargs) { return yargs .example('TEST_ELASTICSEARCH=\'true\' ELASTICSEARCH_PORT=\'9200\' $0 k8s-env', 'Start a kind kubernetes cluster running teraslice from your local repository and elasticsearch.') + .example('TEST_ELASTICSEARCH=\'true\' ELASTICSEARCH_PORT=\'9200\' $0 k8s-env --dev', 'Start a kind kubernetes cluster running teraslice in dev mode. Faster build times.') .example('TEST_ELASTICSEARCH=\'true\' ELASTICSEARCH_PORT=\'9200\' $0 k8s-env --teraslice-image=terascope/teraslice:v0.91.0-nodev18.18.2', 'Start a kind kubernetes cluster running teraslice from a specific docker image and elasticsearch.') .example('TEST_ELASTICSEARCH=\'true\' ELASTICSEARCH_PORT=\'9200\' TEST_KAFKA=\'true\' KAFKA_PORT=\'9092\' $0 k8s-env', 'Start a kind kubernetes cluster running teraslice, elasticsearch, kafka, and zookeeper.') .example('TEST_ELASTICSEARCH=\'true\' ELASTICSEARCH_PORT=\'9200\' $0 k8s-env --skip-build', 'Start a kind kubernetes cluster, but skip building a new teraslice docker image.') @@ -93,6 +94,11 @@ const cmd: CommandModule = { default: 'elasticsearch-next', choices: ['elasticsearch-next', 's3'], }) + .option('dev', { + description: 'Mounts local teraslice to k8s resources for faster development.', + type: 'boolean', + default: false + }) .check((args) => { if (args['asset-storage'] === 's3' && process.env.TEST_MINIO !== 'true') { throw new Error('You chose "s3" as an asset storage but don\'t have the minio service enabled.\n' @@ -118,7 +124,8 @@ const cmd: CommandModule = { kindClusterName: argv['cluster-name'] as string, k8sVersion: argv['k8s-version'] as string, terasliceImage: argv['teraslice-image'] as string, - assetStorage: argv['asset-storage'] as string + assetStorage: argv['asset-storage'] as string, + dev: Boolean(argv.dev) }; if (Boolean(argv.rebuild) === true) { diff --git a/packages/scripts/src/helpers/interfaces.ts b/packages/scripts/src/helpers/interfaces.ts index c3dadfabdc0..e3cfaa16239 100644 --- a/packages/scripts/src/helpers/interfaces.ts +++ b/packages/scripts/src/helpers/interfaces.ts @@ -1,4 +1,5 @@ import { EmptyObject } from '@terascope/utils'; +import type { V1Volume, V1VolumeMount } from '@kubernetes/client-node'; export type PackageInfo = { dir: string; @@ -157,3 +158,9 @@ export type KindCluster = { } ] } + +export interface TsVolumeSet { + extraMounts: any[], + volumes: V1Volume[], + volumeMounts: V1VolumeMount[] +} diff --git a/packages/scripts/src/helpers/k8s-env/index.ts b/packages/scripts/src/helpers/k8s-env/index.ts index 220346ec97d..19ccc5ead1c 100644 --- a/packages/scripts/src/helpers/k8s-env/index.ts +++ b/packages/scripts/src/helpers/k8s-env/index.ts @@ -1,3 +1,4 @@ +import execa from 'execa'; import { dockerTag, isKindInstalled, @@ -20,7 +21,6 @@ const e2eImage = `${rootInfo.name}:e2e-nodev${config.NODE_VERSION}`; export async function launchK8sEnv(options: K8sEnvOptions) { signale.pending('Starting k8s environment with the following options: ', options); - const kind = new Kind(options.k8sVersion, options.kindClusterName); // TODO: create a kind class const kindInstalled = await isKindInstalled(); @@ -36,9 +36,26 @@ export async function launchK8sEnv(options: K8sEnvOptions) { process.exit(1); } + // If --dev is true, we must run yarn setup before creating resources + // We need a local node_modules folder built to add it as a volume + if (options.dev) { + if (process.version.substring(1) !== options.nodeVersion) { + throw new Error(`The node version this process is running on (${process.version}) does not match + the --node-version set in k8s-env (v${options.nodeVersion}). Check your version by running "node -v"`); + } + signale.info(`Running yarn setup with node ${process.version}...`); + try { + execa.commandSync('yarn setup'); + } catch (err) { + signale.fatal(err); + await kind.destroyCluster(); + process.exit(1); + } + } + signale.pending('Creating kind cluster'); try { - await kind.createCluster(options.tsPort); + await kind.createCluster(options.tsPort, options.dev); } catch (err) { signale.error(err); // Do not destroy existing cluster if that was the cause of failure @@ -144,7 +161,8 @@ async function buildAndTagTerasliceImage(options:K8sEnvOptions) { dryRun: true, nodeSuffix: true, nodeVersion: options.nodeVersion, - type: PublishType.Dev + type: PublishType.Dev, + useDevFile: options.dev }; runImage = await buildDevDockerImage(publishOptions); } catch (err) { diff --git a/packages/scripts/src/helpers/k8s-env/interfaces.ts b/packages/scripts/src/helpers/k8s-env/interfaces.ts index e90b8a7003a..48d52d28d88 100644 --- a/packages/scripts/src/helpers/k8s-env/interfaces.ts +++ b/packages/scripts/src/helpers/k8s-env/interfaces.ts @@ -15,6 +15,7 @@ export interface K8sEnvOptions { assetStorage: string; terasliceImage?: string; resetStore?: boolean; + dev: boolean; } // TODO: create a common parent for each resource type, diff --git a/packages/scripts/src/helpers/k8s-env/k8s.ts b/packages/scripts/src/helpers/k8s-env/k8s.ts index cd2d34938eb..7ff2170fae2 100644 --- a/packages/scripts/src/helpers/k8s-env/k8s.ts +++ b/packages/scripts/src/helpers/k8s-env/k8s.ts @@ -8,6 +8,7 @@ import { getE2eK8sDir } from '../../helpers/packages'; import { K8sEnvOptions } from './interfaces'; import signale from '../signale'; import * as config from '../config'; +import { getVolumesFromDockerfile } from '../kind'; const logger = debugLogger('ts-scripts:k8s-env'); export class K8s { @@ -58,6 +59,35 @@ export class K8s { } } + mountLocalTeraslice(masterDeployment: k8sClient.V1Deployment) { + const dockerfileMounts = getVolumesFromDockerfile(true, logger); + if (masterDeployment.spec?.template.spec?.containers[0].volumeMounts) { + masterDeployment.spec.template.spec.containers[0].volumeMounts + .push(...dockerfileMounts.volumeMounts); + } + if (masterDeployment.spec?.template.spec?.volumes) { + masterDeployment.spec.template.spec.volumes + .push(...dockerfileMounts.volumes); + } + + /// Pass in env so master passes volumes to ex's and workers + if (masterDeployment.spec?.template.spec?.containers[0].env) { + masterDeployment.spec.template.spec.containers[0].env.push( + { + name: 'MOUNT_LOCAL_TERASLICE', + value: JSON.stringify(dockerfileMounts) + } + ); + } else if (masterDeployment.spec?.template.spec?.containers[0]) { + masterDeployment.spec.template.spec.containers[0].env = [ + { + name: 'MOUNT_LOCAL_TERASLICE', + value: JSON.stringify(dockerfileMounts) + } + ]; + } + } + async deployK8sTeraslice(wait = false, options: K8sEnvOptions | undefined = undefined) { signale.pending('Begin teraslice deployment...'); const e2eK8sDir = getE2eK8sDir(); @@ -76,7 +106,7 @@ export class K8s { masterTerafoundation.teraslice.kubernetes_image = `teraslice-workspace:e2e-nodev${config.NODE_VERSION}`; if (options) { const storageType = options.assetStorage; - masterTerafoundation.terafoundation.asset_storage_connection_type = storageType; + masterTerafoundation.teraslice.asset_storage_connection_type = storageType; } masterConfigMap.data = { 'teraslice.yaml': k8sClient.dumpYaml(masterTerafoundation) }; masterConfigMap.metadata = { name: 'teraslice-master' }; @@ -94,7 +124,7 @@ export class K8s { workerTerafoundation.teraslice.kubernetes_image = `teraslice-workspace:e2e-nodev${config.NODE_VERSION}`; if (options) { const storageType = options.assetStorage; - workerTerafoundation.terafoundation.asset_storage_connection_type = storageType; + workerTerafoundation.teraslice.asset_storage_connection_type = storageType; } workerConfigMap.data = { 'teraslice.yaml': k8sClient.dumpYaml(workerTerafoundation) }; workerConfigMap.metadata = { name: 'teraslice-worker' }; @@ -114,6 +144,9 @@ export class K8s { if (yamlTSMasterDeployment.spec?.template.spec?.containers[0]) { yamlTSMasterDeployment.spec.template.spec.containers[0].image = `teraslice-workspace:e2e-nodev${config.NODE_VERSION}`; } + if (options?.dev) { + this.mountLocalTeraslice(yamlTSMasterDeployment); + } const response = await this.k8sAppsV1Api.createNamespacedDeployment('ts-dev1', yamlTSMasterDeployment); logger.debug('deployK8sTeraslice yamlTSMasterDeployment: ', response.body); } catch (err) { diff --git a/packages/scripts/src/helpers/kind.ts b/packages/scripts/src/helpers/kind.ts index 7ec751598f9..37733e1fe32 100644 --- a/packages/scripts/src/helpers/kind.ts +++ b/packages/scripts/src/helpers/kind.ts @@ -4,9 +4,10 @@ import path from 'path'; import execa from 'execa'; import yaml from 'js-yaml'; import { Logger, debugLogger } from '@terascope/utils'; +import type { V1Volume, V1VolumeMount } from '@kubernetes/client-node'; import signale from './signale'; import { getE2eK8sDir } from '../helpers/packages'; -import { KindCluster } from './interfaces'; +import { KindCluster, TsVolumeSet } from './interfaces'; import { TERASLICE_PORT } from './config'; export class Kind { @@ -22,7 +23,7 @@ export class Kind { this.k8sVersion = k8sVersion; } - async createCluster(teraslicePort = TERASLICE_PORT): Promise { + async createCluster(teraslicePort = TERASLICE_PORT, devMode: boolean = false): Promise { this.kindVersion = await this.getKindVersion(); const e2eK8sDir = getE2eK8sDir(); @@ -33,10 +34,10 @@ export class Kind { let configPath: string; // clusterName must match 'name' in kind config yaml file - if (this.clusterName === 'k8s-env') { - configPath = path.join(e2eK8sDir, 'kindConfigDefaultPorts.yaml'); - } else if (this.clusterName === 'k8s-e2e') { + if (this.clusterName === 'k8s-e2e') { configPath = path.join(e2eK8sDir, 'kindConfigTestPorts.yaml'); + } else if (this.clusterName === 'k8s-env') { + configPath = path.join(e2eK8sDir, 'kindConfigDefaultPorts.yaml'); } else { signale.error(`No config file for cluster with name ${this.clusterName}`); process.exit(1); @@ -48,6 +49,10 @@ export class Kind { } if (configFile.nodes[0].extraMounts) { configFile.nodes[0].extraMounts[0].hostPath = path.join(e2eK8sDir, '..', 'autoload'); + if (devMode) { + const dockerFileMounts = getVolumesFromDockerfile(true, this.logger).extraMounts; + configFile.nodes[0].extraMounts.push(...dockerFileMounts); + } } configFile.nodes[0].extraPortMappings[1].hostPort = Number.parseInt(teraslicePort, 10); const updatedYaml = yaml.dump(configFile); @@ -96,6 +101,94 @@ export class Kind { } } +export function getVolumesFromDockerfile( + mountNodeModules: boolean, + logger: Logger, + dockerfilePath = path.join(process.cwd(), 'Dockerfile') +):TsVolumeSet { + const finalResult:TsVolumeSet = { + extraMounts: [], + volumes: [], + volumeMounts: [] + }; + try { + logger.debug(`Reading Dockerfile at path: ${dockerfilePath}`); + const dockerfile = fs.readFileSync(dockerfilePath, 'utf-8'); + + const dockerfileArray = dockerfile.split(/\r?\n/); + + const copyLines = dockerfileArray.filter((line) => { + if (line.substring(0, 4) === 'COPY') { + return true; + } + return false; + }).map((value) => value.slice(5).split(' ')); + + if (mountNodeModules) { + copyLines.push(['node_modules', '/app/source/node_modules']); + } + // Grab all files/directories found in dockerfile to show in debugger + const foundVolumes = []; + for (const line of copyLines) { + foundVolumes.push(...line.slice(0, -1)); + } + logger.info(`Found the following files/directories to be used as volume Mounts: ${foundVolumes}`); + + /// Check if directory or file + for (const line of copyLines) { + for (let index = 0; index < line.length - 1; index++) { + const exMount:any = { + hostPath: '', + containerPath: '' + }; + const volume:V1Volume = { + name: '' + }; + const volumeMount:V1VolumeMount = { + name: '', + mountPath: '' + }; + const currentMount = line[index]; + const containerDir = line[line.length - 1]; + const fileStat = fs.statSync(currentMount); + + // Map exMount + exMount.hostPath = `./${currentMount}`; + // Must be an absolute path + exMount.containerPath = currentMount.substring(0, 1) === '/' ? currentMount : `/${currentMount}`; + + // remove all '/', '_' and '.' from name + volumeMount.name = currentMount.replace(/[./_]/g, ''); + + volume.name = volumeMount.name; + if (fileStat.isFile()) { + volume.hostPath = { + path: exMount.containerPath, + type: 'File' + }; + /// If it's a file we need to map the path with the file name + volumeMount.mountPath = path.join(containerDir, currentMount); + // volumeMount.mountPath = containerDir; + } else if (fileStat.isDirectory()) { + volume.hostPath = { + path: exMount.containerPath, + type: 'Directory' + }; + volumeMount.mountPath = containerDir; + } else { + throw new Error(`Path ${line[index]} is neither a file or directory`); + } + finalResult.extraMounts.push(exMount); + finalResult.volumeMounts.push(volumeMount); + finalResult.volumes.push(volume); + } + } + } catch (err) { + throw new Error(`Failed to extract Docker volumes from Dockerfile. Reason: ${err}`); + } + return finalResult; +} + const kindToK8sVersionMap = { '0.20.0': { '1.28.0': 'kindest/node:v1.28.0@sha256:b7a4cad12c197af3ba43202d3efe03246b3f0793f162afb40a33c923952d5b31', diff --git a/packages/scripts/src/helpers/publish/interfaces.ts b/packages/scripts/src/helpers/publish/interfaces.ts index 0373b7bd576..87077be5c28 100644 --- a/packages/scripts/src/helpers/publish/interfaces.ts +++ b/packages/scripts/src/helpers/publish/interfaces.ts @@ -20,4 +20,5 @@ export interface PublishOptions { */ publishOutdatedPackages?: boolean; nodeVersion?: string; + useDevFile?: boolean; } diff --git a/packages/scripts/src/helpers/publish/utils.ts b/packages/scripts/src/helpers/publish/utils.ts index 1bd0ae4c10e..fbaecb94c4c 100644 --- a/packages/scripts/src/helpers/publish/utils.ts +++ b/packages/scripts/src/helpers/publish/utils.ts @@ -97,7 +97,7 @@ export async function buildDevDockerImage( signale.pending(`building docker image ${devImage}`); try { - await dockerBuild(devImage, cacheFromPrev ? [devImage] : [], undefined, `NODE_VERSION=${publishOptions.nodeVersion}`); + await dockerBuild(devImage, cacheFromPrev ? [devImage] : [], undefined, `NODE_VERSION=${publishOptions.nodeVersion}`, publishOptions.useDevFile); } catch (err) { throw new TSError(err, { message: `Failed to build ${devImage} docker image`, diff --git a/packages/scripts/src/helpers/scripts.ts b/packages/scripts/src/helpers/scripts.ts index 591b008ac4e..72268e93172 100644 --- a/packages/scripts/src/helpers/scripts.ts +++ b/packages/scripts/src/helpers/scripts.ts @@ -393,7 +393,8 @@ export async function dockerBuild( tag: string, cacheFrom?: string[], target?: string, - buildArg?: string + buildArg?: string, + useDevFile?: boolean ): Promise { const cacheFromArgs: string[] = []; @@ -403,10 +404,11 @@ export async function dockerBuild( const targetArgs: string[] = target ? ['--target', target] : []; const buildsArgs: string[] = buildArg ? ['--build-arg', buildArg] : []; + const dockerFilePath = useDevFile ? ['-f', 'Dockerfile.dev', '.'] : ['.']; await fork({ cmd: 'docker', - args: ['build', ...cacheFromArgs, ...targetArgs, ...buildsArgs, '--tag', tag, '.'], + args: ['build', ...cacheFromArgs, ...targetArgs, ...buildsArgs, '--tag', tag, ...dockerFilePath], }); } diff --git a/packages/teraslice/src/lib/cluster/services/cluster/backends/kubernetes/k8sResource.ts b/packages/teraslice/src/lib/cluster/services/cluster/backends/kubernetes/k8sResource.ts index 26253e62861..6eecc88f754 100644 --- a/packages/teraslice/src/lib/cluster/services/cluster/backends/kubernetes/k8sResource.ts +++ b/packages/teraslice/src/lib/cluster/services/cluster/backends/kubernetes/k8sResource.ts @@ -85,6 +85,9 @@ export class K8sResource { this._setTargets(); this._setResources(); this._setVolumes(); + if (process.env.MOUNT_LOCAL_TERASLICE !== undefined) { + this._mountLocalTeraslice(resourceName); + } this._setAssetsVolume(); this._setImagePullSecret(); this._setEphemeralStorage(); @@ -108,6 +111,20 @@ export class K8sResource { } } + _mountLocalTeraslice(contextType: string) { + const devMounts = JSON.parse(process.env.MOUNT_LOCAL_TERASLICE as string); + this.resource.spec.template.spec.containers[0].volumeMounts.push(...devMounts.volumeMounts); + this.resource.spec.template.spec.volumes.push(...devMounts.volumes); + + if (contextType === 'execution_controller') { + this.resource.spec.template.spec.containers[0].args = [ + 'yarn', + 'node', + 'service.js' + ]; + } + } + _makeConfig(): K8sConfig { const clusterName = _.get(this.terasliceConfig, 'name'); const clusterNameLabel = clusterName.replace(/[^a-zA-Z0-9_\-.]/g, '_').substring(0, 63); diff --git a/yarn.lock b/yarn.lock index c662f07272e..abf27966556 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3449,7 +3449,7 @@ any-promise@^1.3.0: resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== -anymatch@^3.0.3: +anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -3918,6 +3918,11 @@ bin-version@^3.0.0: execa "^1.0.0" find-versions "^3.0.0" +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + binaryextensions@^4.15.0, binaryextensions@^4.16.0: version "4.19.0" resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-4.19.0.tgz#7944b41ce6bbbcd3e544e05f65794ac48caaa132" @@ -4012,7 +4017,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.3: +braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -4364,6 +4369,21 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chownr@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" @@ -4960,7 +4980,7 @@ datemath-parser@^1.0.6: dependencies: moment "^2.22.2" -debug@2.2.0, debug@2.3.3, debug@2.6.9, debug@4, debug@4.x, debug@^2.2.0, debug@^3.2.7, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1: +debug@2.2.0, debug@2.3.3, debug@2.6.9, debug@4, debug@4.x, debug@^2.2.0, debug@^3.2.7, debug@^4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -6315,7 +6335,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2: +fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -6533,7 +6553,7 @@ git-hooks-list@1.0.3: resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-1.0.3.tgz#be5baaf78203ce342f2f844a9d2b03dba1b45156" integrity sha512-Y7wLWcrLUXwk2noSka166byGCvhMtDRpgHdzCno1UQv/n/Hegp++a2xBWJL1lJarnKD3SWaljD+0z1ztqxuKyQ== -glob-parent@^5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -7078,6 +7098,11 @@ ieee754@^1.1.12, ieee754@^1.1.13, ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + ignore-walk@3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" @@ -7341,6 +7366,13 @@ is-bigint@^1.0.1: dependencies: has-bigints "^1.0.1" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" @@ -7440,7 +7472,7 @@ is-generator-function@^1.0.10: dependencies: has-tostringtag "^1.0.0" -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -9331,6 +9363,22 @@ node-webhdfs@^1.0.2: lodash "^4.17.10" request "^2.87.0" +nodemon@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.4.tgz#c34dcd8eb46a05723ccde60cbdd25addcc8725e4" + integrity sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + noop6@^1.0.1: version "1.0.9" resolved "https://registry.yarnpkg.com/noop6/-/noop6-1.0.9.tgz#8749944c15c09f2cd2d562ac24f5a8341762a950" @@ -9353,7 +9401,7 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package- semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -10154,7 +10202,7 @@ picocolors@^1.0.0, picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== -picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -10363,6 +10411,11 @@ psl@^1.1.28: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -10587,6 +10640,13 @@ readdir-scoped-modules@^1.1.0: graceful-fs "^4.1.2" once "^1.3.0" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -11160,6 +11220,13 @@ signale@^1.4.0: figures "^2.0.0" pkg-conf "^2.1.0" +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -11699,7 +11766,7 @@ supports-color@^3.1.2: dependencies: has-flag "^1.0.0" -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -11906,6 +11973,11 @@ toposort@^2.0.2: resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -12197,6 +12269,11 @@ unbzip2-stream@^1.0.9: buffer "^5.2.1" through "^2.3.8" +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + underscore@^1.9.1: version "1.13.6" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441"