From 854509c5192a708f822b35ada7219bd157149d00 Mon Sep 17 00:00:00 2001 From: Jon Edvald Date: Tue, 8 Sep 2020 17:05:54 +0200 Subject: [PATCH] feat(cli): add `get module(s)` command Closes #2038 --- core/src/commands/get/get-modules.ts | 89 ++++++ core/src/commands/get/get.ts | 2 + .../test/unit/src/commands/get/get-modules.ts | 79 +++++ docs/reference/commands.md | 272 ++++++++++++++++++ package.json | 1 + 5 files changed, 443 insertions(+) create mode 100644 core/src/commands/get/get-modules.ts create mode 100644 core/test/unit/src/commands/get/get-modules.ts diff --git a/core/src/commands/get/get-modules.ts b/core/src/commands/get/get-modules.ts new file mode 100644 index 0000000000..a9753e3c4c --- /dev/null +++ b/core/src/commands/get/get-modules.ts @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2018-2020 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Command, CommandParams, PrepareParams } from "../base" +import { StringsParameter, BooleanParameter } from "../../cli/params" +import { moduleSchema, GardenModule } from "../../types/module" +import { keyBy, omit, sortBy } from "lodash" +import { joiIdentifierMap, joi } from "../../config/common" +import { printHeader } from "../../logger/util" +import chalk from "chalk" +import { renderTable, dedent } from "../../util/string" +import { relative, sep } from "path" + +const getModulesArgs = { + modules: new StringsParameter({ + help: + "Specify module(s) to list. Use comma as a separator to specify multiple modules. Skip to return all modules.", + }), +} + +const getModulesOptions = { + "exclude-disabled": new BooleanParameter({ + help: "Exclude disabled modules from output.", + }), +} + +type Args = typeof getModulesArgs +type Opts = typeof getModulesOptions + +type OutputModule = Omit + +export class GetModulesCommand extends Command { + name = "modules" + alias = "module" + help = "Outputs all or specified modules." + description = dedent` + Outputs all or specified modules. Use with --output=json and jq to extract specific fields. + + Examples: + + garden get modules # list all modules in the project + garden get modules --exclude-disabled=true # skip disabled modules + garden get modules -o=json | jq '.modules["my-module"].version' # get version of my-module + ` + + arguments = getModulesArgs + options = getModulesOptions + + outputsSchema = () => joi.object().keys({ modules: joiIdentifierMap(moduleSchema()) }) + + async prepare({ headerLog }: PrepareParams) { + printHeader(headerLog, "Get Modules", "open_book") + return { persistent: false } + } + async action({ garden, log, args, opts }: CommandParams) { + const graph = await garden.getConfigGraph(log) + + const modules: OutputModule[] = sortBy( + graph + .getModules({ names: args.modules, includeDisabled: !opts["exclude-disabled"] }) + .map((m) => omit(m, "_config")), + "name" + ) + + const modulesByName = keyBy(modules, "name") + + const heading = ["Name", "Version", "Type", "Path"].map((s) => chalk.bold(s)) + const rows: string[][] = modules.map((m: OutputModule) => { + const relPath = relative(garden.projectRoot, m.path) + + return [ + chalk.cyan.bold(m.name), + m.version.versionString, + m.type, + relPath.startsWith("..") ? relPath : "." + sep + relPath, + ] + }) + + log.info("") + log.info(renderTable([heading].concat(rows))) + + return { result: { modules: modulesByName } } + } +} diff --git a/core/src/commands/get/get.ts b/core/src/commands/get/get.ts index 765a9949f2..ab63d2326a 100644 --- a/core/src/commands/get/get.ts +++ b/core/src/commands/get/get.ts @@ -19,6 +19,7 @@ import { GetDebugInfoCommand } from "./get-debug-info" import { GetLinkedReposCommand } from "./get-linked-repos" import { GetOutputsCommand } from "./get-outputs" import { GetDoddiCommand } from "./get-doddi" +import { GetModulesCommand } from "./get-modules" export class GetCommand extends CommandGroup { name = "get" @@ -31,6 +32,7 @@ export class GetCommand extends CommandGroup { GetEysiCommand, GetLinkedReposCommand, GetOutputsCommand, + GetModulesCommand, GetSecretCommand, GetStatusCommand, GetTasksCommand, diff --git a/core/test/unit/src/commands/get/get-modules.ts b/core/test/unit/src/commands/get/get-modules.ts new file mode 100644 index 0000000000..482becc79f --- /dev/null +++ b/core/test/unit/src/commands/get/get-modules.ts @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018-2020 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { expect } from "chai" +import { keyBy, omit, mapValues } from "lodash" +import { makeTestGardenA, withDefaultGlobalOpts } from "../../../../helpers" +import { GetModulesCommand } from "../../../../../src/commands/get/get-modules" + +describe("GetModulesCommand", () => { + const command = new GetModulesCommand() + + it("returns all modules in a project", async () => { + const garden = await makeTestGardenA() + const log = garden.log + + const res = await command.action({ + garden, + log, + headerLog: log, + footerLog: log, + args: { modules: undefined }, + opts: withDefaultGlobalOpts({ "exclude-disabled": false }), + }) + + expect(command.outputsSchema().validate(res.result).error).to.be.undefined + + const expected = mapValues(keyBy(await garden.resolveModules({ log }), "name"), (m) => omit(m, ["_config"])) + + expect(res.result).to.eql({ modules: expected }) + }) + + it("skips disabled modules if exclude-disabled=true", async () => { + const garden = await makeTestGardenA() + const log = garden.log + + await garden.scanAndAddConfigs() + garden["moduleConfigs"]["module-a"].disabled = true + + const res = await command.action({ + garden, + log, + headerLog: log, + footerLog: log, + args: { modules: undefined }, + opts: withDefaultGlobalOpts({ "exclude-disabled": true }), + }) + + expect(command.outputsSchema().validate(res.result).error).to.be.undefined + + expect(res.result?.modules["module-a"]).to.not.exist + expect(res.result?.modules["module-b"]).to.exist + }) + + it("returns specified module in a project", async () => { + const garden = await makeTestGardenA() + const log = garden.log + + const res = await command.action({ + garden, + log, + headerLog: log, + footerLog: log, + args: { modules: ["module-a"] }, + opts: withDefaultGlobalOpts({ "exclude-disabled": false }), + }) + + expect(command.outputsSchema().validate(res.result).error).to.be.undefined + + const graph = await garden.getConfigGraph(log) + const moduleA = graph.getModule("module-a") + + expect(res.result).to.eql({ modules: { "module-a": omit(moduleA, ["_config"]) } }) + }) +}) diff --git a/docs/reference/commands.md b/docs/reference/commands.md index be22804519..f79deae626 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -1313,6 +1313,278 @@ Examples: : ``` +### garden get modules + +**Outputs all or specified modules.** + +Outputs all or specified modules. Use with --output=json and jq to extract specific fields. + +Examples: + + garden get modules # list all modules in the project + garden get modules --exclude-disabled=true # skip disabled modules + garden get modules -o=json | jq '.modules["my-module"].version' # get version of my-module + +| Supported in workflows | | +| ---------------------- |---| +| No | | + +#### Usage + + garden get modules [modules] [options] + +#### Arguments + +| Argument | Required | Description | +| -------- | -------- | ----------- | + | `modules` | No | Specify module(s) to list. Use comma as a separator to specify multiple modules. Skip to return all modules. + +#### Options + +| Argument | Alias | Type | Description | +| -------- | ----- | ---- | ----------- | + | `--exclude-disabled` | | boolean | Exclude disabled modules from output. + +#### Outputs + +```yaml +# Key/value map. Keys must be valid identifiers. +modules: + # The configuration for a module. + : + # The schema version of this module's config (currently not used). + apiVersion: + + kind: + + # The type of this module. + type: + + # The name of this module. + name: + + # A description of the module. + description: + + # Set this to `true` to disable the module. You can use this with conditional template strings to disable modules + # based on, for example, the current environment or other variables (e.g. `disabled: \${environment.name == + # "prod"}`). This can be handy when you only need certain modules for specific environments, e.g. only for + # development. + # + # Disabling a module means that any services, tasks and tests contained in it will not be deployed or run. It also + # means that the module is not built _unless_ it is declared as a build dependency by another enabled module (in + # which case building this module is necessary for the dependant to be built). + # + # If you disable the module, and its services, tasks or tests are referenced as _runtime_ dependencies, Garden + # will automatically ignore those dependency declarations. Note however that template strings referencing the + # module's service or task outputs (i.e. runtime outputs) will fail to resolve when the module is disabled, so you + # need to make sure to provide alternate values for those if you're using them, using conditional expressions. + disabled: + + # Specify a list of POSIX-style paths or globs that should be regarded as the source files for this module. Files + # that do *not* match these paths or globs are excluded when computing the version of the module, when responding + # to filesystem watch events, and when staging builds. + # + # Note that you can also _exclude_ files using the `exclude` field or by placing `.gardenignore` files in your + # source tree, which use the same format as `.gitignore` files. See the [Configuration Files + # guide](https://docs.garden.io/using-garden/configuration-overview#including-excluding-files-and-directories) for + # details. + # + # Also note that specifying an empty list here means _no sources_ should be included. + include: + + # Specify a list of POSIX-style paths or glob patterns that should be excluded from the module. Files that match + # these paths or globs are excluded when computing the version of the module, when responding to filesystem watch + # events, and when staging builds. + # + # Note that you can also explicitly _include_ files using the `include` field. If you also specify the `include` + # field, the files/patterns specified here are filtered from the files matched by `include`. See the + # [Configuration Files + # guide](https://docs.garden.io/using-garden/configuration-overview#including-excluding-files-and-directories) for + # details. + # + # Unlike the `modules.exclude` field in the project config, the filters here have _no effect_ on which files and + # directories are watched for changes. Use the project `modules.exclude` field to affect those, if you have large + # directories that should not be watched for changes. + exclude: + + # A remote repository URL. Currently only supports git servers. Must contain a hash suffix pointing to a specific + # branch or tag, with the format: # + # + # Garden will import the repository source code into this module, but read the module's config from the local + # garden.yml file. + repositoryUrl: + + # When false, disables pushing this module to remote registries. + allowPublish: + + # Specify how to build the module. Note that plugins may define additional keys on this object. + build: + # A list of modules that must be built before this module is built. + dependencies: + - # Module name to build ahead of this module. + name: + + # Specify one or more files or directories to copy from the built dependency to this module. + copy: + - # POSIX-style path or filename of the directory or file(s) to copy to the target. + source: + + # POSIX-style path or filename to copy the directory or file(s), relative to the build directory. + # Defaults to to same as source path. + target: + + # The filesystem path of the module. + path: + + # List of services configured by this module. + serviceConfigs: + - # Valid RFC1035/RFC1123 (DNS) label (may contain lowercase letters, numbers and dashes, must start with a + # letter, and cannot end with a dash), cannot contain consecutive dashes or start with `garden`, or be longer + # than 63 characters. + name: + + # The names of any services that this service depends on at runtime, and the names of any tasks that should be + # executed before this service is deployed. + dependencies: + + # Set this to `true` to disable the service. You can use this with conditional template strings to + # enable/disable services based on, for example, the current environment or other variables (e.g. `enabled: + # \${environment.name != "prod"}`). This can be handy when you only need certain services for specific + # environments, e.g. only for development. + # + # Disabling a service means that it will not be deployed, and will also be ignored if it is declared as a + # runtime dependency for another service, test or task. + # + # Note however that template strings referencing the service's outputs (i.e. runtime outputs) will fail to + # resolve when the service is disabled, so you need to make sure to provide alternate values for those if + # you're using them, using conditional expressions. + disabled: + + # Set this to true if the module and service configuration supports hot reloading. + hotReloadable: + + # The `validate` module action should populate this, if the service's code sources are contained in a separate + # module from the parent module. For example, when the service belongs to a module that contains manifests + # (e.g. a Helm chart), but the actual code lives in a different module (e.g. a container module). + sourceModuleName: + + # The service's specification, as defined by its provider plugin. + spec: + + # List of tasks configured by this module. + taskConfigs: + - # The name of the task. + name: + + # A description of the task. + description: + + # The names of any tasks that must be executed, and the names of any services that must be running, before + # this task is executed. + dependencies: + + # Set this to `true` to disable the task. You can use this with conditional template strings to enable/disable + # tasks based on, for example, the current environment or other variables (e.g. `enabled: \${environment.name + # != "prod"}`). This can be handy when you only want certain tasks to run in specific environments, e.g. only + # for development. + # + # Disabling a task means that it will not be run, and will also be ignored if it is declared as a runtime + # dependency for another service, test or task. + # + # Note however that template strings referencing the task's outputs (i.e. runtime outputs) will fail to + # resolve when the task is disabled, so you need to make sure to provide alternate values for those if you're + # using them, using conditional expressions. + disabled: + + # Maximum duration (in seconds) of the task's execution. + timeout: + + # Set to false if you don't want the task's result to be cached. Use this if the task needs to be run any time + # your project (or one or more of the task's dependants) is deployed. Otherwise the task is only re-run when + # its version changes (i.e. the module or one of its dependencies is modified), or when you run `garden run + # task`. + cacheResult: + + # The task's specification, as defined by its provider plugin. + spec: + + # List of tests configured by this module. + testConfigs: + - # The name of the test. + name: + + # The names of any services that must be running, and the names of any tasks that must be executed, before the + # test is run. + dependencies: + + # Set this to `true` to disable the test. You can use this with conditional template strings to + # enable/disable tests based on, for example, the current environment or other variables (e.g. + # `enabled: \${environment.name != "prod"}`). This is handy when you only want certain tests to run in + # specific environments, e.g. only during CI. + disabled: + + # Maximum duration (in seconds) of the test run. + timeout: + + # The configuration for the test, as specified by its module's provider. + spec: + + # The module spec, as defined by the provider plugin. + spec: + + # The path to the build staging directory for the module. + buildPath: + + # The path to the build metadata directory for the module. + buildMetadataPath: + + # A list of types that this module is compatible with (i.e. the module type itself + all bases). + compatibleTypes: + + # The path to the module config file, if applicable. + configPath: + + version: + # String representation of the module version. + versionString: + + # The version of each of the dependencies of the module. + dependencyVersions: + : + # The hash of all files in the directory, after filtering. + contentHash: + + # List of file paths included in the version. + files: + + # List of file paths included in the version. + files: + + # A map of all modules referenced under `build.dependencies`. + buildDependencies: + : + + # Indicate whether the module needs to be built (i.e. has a build handler or needs to copy dependencies). + needsBuild: + + # The outputs defined by the module (referenceable in other module configs). + outputs: + : + + # The names of the services that the module provides. + serviceNames: + + # The names of all the services and tasks that the services in this module depend on. + serviceDependencyNames: + + # The names of the tasks that the module provides. + taskNames: + + # The names of all the tasks and services that the tasks in this module depend on. + taskDependencyNames: +``` + ### garden get status **Outputs the full status of your environment.** diff --git a/package.json b/package.json index bea25a5b88..b4e540e23b 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "check-licenses": "gulp check-licenses", "check-package-lock": "git diff --quiet HEAD -- yarn.lock || (echo 'yarn.lock is dirty!' && exit 1)", "clean": "./scripts/run-script.ts clean && find . -name \".garden\" -type d -prune -exec rm -rf '{}' '+'", + "clean-node-modules": "find . -name \"node_modules\" -type d -prune -exec rm -rf '{}' '+'", "dev": "./scripts/run-script.ts dev --parallel", "dist": "./scripts/run-script.ts dist", "fix-format": "./scripts/run-script.ts fix-format --no-bail --parallel",