diff --git a/docs/reference/module-types/container.md b/docs/reference/module-types/container.md index 2a9a65e376..77a0806f7d 100644 --- a/docs/reference/module-types/container.md +++ b/docs/reference/module-types/container.md @@ -265,7 +265,7 @@ hotReload: ### `dockerfile` -POSIX-style name of Dockerfile, relative to project root. Defaults to $MODULE_ROOT/Dockerfile. +POSIX-style name of Dockerfile, relative to module root. | Type | Required | | -------- | -------- | diff --git a/docs/reference/module-types/maven-container.md b/docs/reference/module-types/maven-container.md index 08050ecbfd..51ba3d64d7 100644 --- a/docs/reference/module-types/maven-container.md +++ b/docs/reference/module-types/maven-container.md @@ -270,7 +270,7 @@ hotReload: ### `dockerfile` -POSIX-style name of Dockerfile, relative to project root. Defaults to $MODULE_ROOT/Dockerfile. +POSIX-style name of Dockerfile, relative to module root. | Type | Required | | -------- | -------- | @@ -890,7 +890,7 @@ Key/value map of environment variables. Keys must be valid POSIX environment var ### `jarPath` -The path to the packaged JAR artifact, relative to the module directory. +POSIX-style path to the packaged JAR artifact, relative to the module directory. | Type | Required | | -------- | -------- | diff --git a/garden-service/package-lock.json b/garden-service/package-lock.json index decdb4d05b..74aff85275 100644 --- a/garden-service/package-lock.json +++ b/garden-service/package-lock.json @@ -372,6 +372,34 @@ } } }, + "@hapi/address": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.0.0.tgz", + "integrity": "sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw==" + }, + "@hapi/hoek": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-6.2.4.tgz", + "integrity": "sha512-HOJ20Kc93DkDVvjwHyHawPwPkX44sIrbXazAUDiUXaY2R9JwQGo2PhFfnQtdrsIe4igjG2fPgMra7NYw7qhy0A==" + }, + "@hapi/joi": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.0.3.tgz", + "integrity": "sha512-z6CesJ2YBwgVCi+ci8SI8zixoj8bGFn/vZb9MBPbSyoxsS2PnWYjHcyTM17VLK6tx64YVK38SDIh10hJypB+ig==", + "requires": { + "@hapi/address": "2.x.x", + "@hapi/hoek": "6.x.x", + "@hapi/topo": "3.x.x" + } + }, + "@hapi/topo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.0.tgz", + "integrity": "sha512-gZDI/eXOIk8kP2PkUKjWu9RW8GGVd2Hkgjxyr/S7Z+JF+0mr7bAlbw+DkTRxnD580o8Kqxlnba9wvqp5aOHBww==", + "requires": { + "@hapi/hoek": "6.x.x" + } + }, "@kubernetes/client-node": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.10.1.tgz", @@ -616,6 +644,15 @@ "@types/node": "*" } }, + "@types/hapi__joi": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@types/hapi__joi/-/hapi__joi-15.0.2.tgz", + "integrity": "sha512-EsOuX8cbAdSgp/9mo5NoI4vMnZ68c8Jk1fl3tyA07zd9aOq4q4udsJ2/YjhaFw0u2Zp6hBonUBrKEWotZg7PDQ==", + "dev": true, + "requires": { + "@types/hapi__joi": "*" + } + }, "@types/has-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/has-ansi/-/has-ansi-3.0.0.tgz", @@ -638,12 +675,6 @@ "rxjs": ">=6.4.0" } }, - "@types/joi": { - "version": "14.3.3", - "resolved": "https://registry.npmjs.org/@types/joi/-/joi-14.3.3.tgz", - "integrity": "sha512-6gAT/UkIzYb7zZulAbcof3lFxpiD5EI6xBeTvkL1wYN12pnFQ+y/+xl9BvnVgxkmaIDN89xWhGZLD9CvuOtZ9g==", - "dev": true - }, "@types/js-yaml": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.1.tgz", @@ -6013,11 +6044,6 @@ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.6.tgz", "integrity": "sha512-zozTAWM1D6sozHo8kqhfYgsac+B+q0PmsjXeyDrYIHHcBN0zTVT66+s2GW1GZv7DbyaROdLXKdabwS/WqPyIdQ==" }, - "hoek": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", - "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==" - }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -6595,14 +6621,6 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, - "isemail": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", - "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", - "requires": { - "punycode": "2.x.x" - } - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6828,16 +6846,6 @@ "handlebars": "^4.1.2" } }, - "joi": { - "version": "14.3.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-14.3.1.tgz", - "integrity": "sha512-LQDdM+pkOrpAn4Lp+neNIFV3axv1Vna3j38bisbQhETPMANYRbFJFUyOZcOClYvM/hppMhGWuKSFEK9vjrB+bQ==", - "requires": { - "hoek": "6.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - } - }, "join-component": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz", @@ -11419,14 +11427,6 @@ "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", "dev": true }, - "topo": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", - "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", - "requires": { - "hoek": "6.x.x" - } - }, "toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", diff --git a/garden-service/package.json b/garden-service/package.json index 74dfd34289..c24ab4e8b4 100644 --- a/garden-service/package.json +++ b/garden-service/package.json @@ -22,6 +22,7 @@ "static" ], "dependencies": { + "@hapi/joi": "^15.0.3", "@kubernetes/client-node": "0.10.1", "JSONStream": "^1.3.5", "analytics-node": "3.3.0", @@ -60,7 +61,6 @@ "ignore": "^5.1.1", "indent-string": "^4.0.0", "inquirer": "^6.3.1", - "joi": "^14.3.1", "js-yaml": "^3.13.1", "json-diff": "^0.5.4", "json-merge-patch": "^0.2.3", @@ -120,9 +120,9 @@ "@types/dockerode": "^2.5.16", "@types/execa": "^0.9.0", "@types/fs-extra": "^7.0.0", + "@types/hapi__joi": "^15.0.2", "@types/has-ansi": "^3.0.0", "@types/inquirer": "6.0.1", - "@types/joi": "^14.3.3", "@types/js-yaml": "^3.12.1", "@types/json-merge-patch": "0.0.4", "@types/json-stringify-safe": "^5.0.0", diff --git a/garden-service/src/actions.ts b/garden-service/src/actions.ts index f2b0549090..884e25f7fe 100644 --- a/garden-service/src/actions.ts +++ b/garden-service/src/actions.ts @@ -9,12 +9,11 @@ import Bluebird = require("bluebird") import chalk from "chalk" -import * as Joi from "joi" import { fromPairs, keyBy, mapValues, omit, pickBy, values } from "lodash" import { PublishModuleParams, PublishResult } from "./types/plugin/module/publishModule" import { SetSecretParams, SetSecretResult } from "./types/plugin/provider/setSecret" -import { validate } from "./config/common" +import { validate, joi } from "./config/common" import { defaultProvider, Provider } from "./config/provider" import { ConfigurationError, ParameterError, PluginError } from "./exceptions" import { ActionHandlerMap, Garden, ModuleActionHandlerMap, ModuleActionMap, PluginActionMap } from "./garden" @@ -261,8 +260,8 @@ export class ActionHelper implements TypeGuard { moduleType, defaultHandler: async ({ }) => ({ docs: "", - outputsSchema: Joi.object().options({ allowUnknown: true }), - schema: Joi.object().options({ allowUnknown: true }), + outputsSchema: joi.object().options({ allowUnknown: true }), + schema: joi.object().options({ allowUnknown: true }), }), }) diff --git a/garden-service/src/commands/base.ts b/garden-service/src/commands/base.ts index 65e141782f..d2687cb34f 100644 --- a/garden-service/src/commands/base.ts +++ b/garden-service/src/commands/base.ts @@ -6,7 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import Joi = require("joi") +import Joi = require("@hapi/joi") +import stripAnsi from "strip-ansi" import { GardenError, RuntimeError, InternalError, ParameterError } from "../exceptions" import { TaskResults } from "../task-graph" import { LoggerType } from "../logger/logger" @@ -15,7 +16,7 @@ import { Garden } from "../garden" import { LogEntry } from "../logger/log-entry" import { printFooter } from "../logger/util" import { GlobalOptions } from "../cli/cli" -import stripAnsi from "strip-ansi" +import { joi } from "../config/common" export interface ParameterConstructor { help: string, @@ -75,7 +76,7 @@ export abstract class Parameter { export class StringParameter extends Parameter { type = "string" - schema = Joi.string() + schema = joi.string() parseString(input: string) { return input @@ -86,7 +87,7 @@ export class StringParameter extends Parameter { // FIXME: Maybe use a Required type to enforce presence, rather that an option flag? export class StringOption extends Parameter { type = "string" - schema = Joi.string() + schema = joi.string() parseString(input?: string) { return input @@ -99,7 +100,7 @@ export interface StringsConstructor extends ParameterConstructor { export class StringsParameter extends Parameter { type = "array:string" - schema = Joi.array().items(Joi.string()) + schema = joi.array().items(joi.string()) delimiter: string constructor(args: StringsConstructor) { @@ -125,7 +126,7 @@ export class StringsParameter extends Parameter { export class PathParameter extends Parameter { type = "path" - schema = Joi.string().uri({ relativeOnly: true }) + schema = joi.string().posixPath() parseString(input: string) { return input @@ -134,7 +135,7 @@ export class PathParameter extends Parameter { export class PathsParameter extends Parameter { type = "array:path" - schema = Joi.array().items(Joi.string().uri({ relativeOnly: true })) + schema = joi.array().items(joi.string().posixPath()) parseString(input: string) { return input.split(",") @@ -143,7 +144,7 @@ export class PathsParameter extends Parameter { export class IntegerParameter extends Parameter { type = "number" - schema = Joi.number().integer() + schema = joi.number().integer() parseString(input: string) { try { @@ -164,13 +165,13 @@ export interface ChoicesConstructor extends ParameterConstructor { export class ChoicesParameter extends Parameter { type = "choice" choices: string[] - schema = Joi.string() + schema = joi.string() constructor(args: ChoicesConstructor) { super(args) this.choices = args.choices - this.schema = Joi.string().only(args.choices) + this.schema = joi.string().only(args.choices) } parseString(input: string) { @@ -191,7 +192,7 @@ export class ChoicesParameter extends Parameter { export class BooleanParameter extends Parameter { type = "boolean" - schema = Joi.boolean() + schema = joi.boolean() parseString(input: any) { return !!input diff --git a/garden-service/src/config-store.ts b/garden-service/src/config-store.ts index 065c1b958c..3ad58b4fa1 100644 --- a/garden-service/src/config-store.ts +++ b/garden-service/src/config-store.ts @@ -6,13 +6,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import * as yaml from "js-yaml" import { join } from "path" import { ensureFile, readFile } from "fs-extra" import { get, isPlainObject, unset } from "lodash" -import { Primitive, validate, joiArray, joiUserIdentifier, joiPrimitive } from "./config/common" +import { Primitive, validate, joiArray, joiUserIdentifier, joiPrimitive, joi } from "./config/common" import { LocalConfigError } from "./exceptions" import { dumpYaml } from "./util/util" import { @@ -188,23 +187,23 @@ export interface LocalConfig { analytics: AnalyticsLocalConfig } -const kubernetesLocalConfigSchema = Joi.object() +const kubernetesLocalConfigSchema = joi.object() .keys({ "username": joiUserIdentifier().allow("").optional(), - "previous-usernames": Joi.array().items(joiUserIdentifier()).optional(), + "previous-usernames": joi.array().items(joiUserIdentifier()).optional(), }) .meta({ internal: true }) -const linkedSourceSchema = Joi.object() +const linkedSourceSchema = joi.object() .keys({ name: joiUserIdentifier(), - path: Joi.string(), + path: joi.string(), }) .meta({ internal: true }) -const AnalyticsLocalConfigSchema = Joi.object() +const AnalyticsLocalConfigSchema = joi.object() .keys({ - projectId: Joi.string(), + projectId: joi.string(), }).meta({ internal: true }) const localConfigSchemaKeys = { @@ -219,12 +218,12 @@ export const localConfigKeys = Object.keys(localConfigSchemaKeys).reduce((acc, k return acc }, {}) as { [K in keyof typeof localConfigSchemaKeys]: K } -const localConfigSchema = Joi.object() +const localConfigSchema = joi.object() .keys(localConfigSchemaKeys) .meta({ internal: true }) // TODO: we should not be passing this to provider actions -export const configStoreSchema = Joi.object() +export const configStoreSchema = joi.object() .description("Helper class for managing local configuration for plugins.") export class LocalConfigStore extends ConfigStore { @@ -259,11 +258,11 @@ export interface GlobalConfig { analytics?: AnalyticsGlobalConfig } -const AnalyticsGlobalConfigSchema = Joi.object() +const AnalyticsGlobalConfigSchema = joi.object() .keys({ userId: joiPrimitive().allow("").optional(), - optedIn: Joi.boolean().optional(), - firstRun: Joi.boolean().optional(), + optedIn: joi.boolean().optional(), + firstRun: joi.boolean().optional(), }).meta({ internal: true }) const globalConfigSchemaKeys = { @@ -284,7 +283,7 @@ export const globalConfigKeys = Object.keys(globalConfigSchemaKeys).reduce((acc, return acc }, {}) as { [K in keyof typeof globalConfigSchemaKeys]: K } -const globalConfigSchema = Joi.object() +const globalConfigSchema = joi.object() .keys(globalConfigSchemaKeys) .meta({ internal: true }) diff --git a/garden-service/src/config/common.ts b/garden-service/src/config/common.ts index aea8a55157..15acf46678 100644 --- a/garden-service/src/config/common.ts +++ b/garden-service/src/config/common.ts @@ -6,8 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { JoiObject } from "joi" -import * as Joi from "joi" +import * as Joi from "@hapi/joi" +import { JoiObject } from "@hapi/joi" import * as uuid from "uuid" import { ConfigurationError, LocalConfigError } from "../exceptions" import chalk from "chalk" @@ -26,10 +26,86 @@ export const enumToArray = Enum => ( Object.values(Enum).filter(k => typeof k === "string") as string[] ) -export const joiPrimitive = () => Joi.alternatives().try( - Joi.number(), - Joi.string().allow("").allow(null), - Joi.boolean(), +interface JoiPathParams { + absoluteOnly?: boolean + relativeOnly?: boolean + subPathOnly?: boolean +} + +// Extend the Joi module with our custom rules +export interface CustomStringSchema extends Joi.StringSchema { + posixPath: (params?: JoiPathParams) => CustomStringSchema +} + +declare module "@hapi/joi" { + export function string(): CustomStringSchema +} + +export const joi: Joi.Root = Joi.extend({ + base: Joi.string(), + name: "string", + language: { + posixPath: "must be a POSIX-style path", // Used below as 'string.posixPath' + absoluteOnly: "must be a an absolute path", + relativeOnly: "must be a relative path (may not be an absolute path)", + subPathOnly: "must be a relative sub-path (may not contain '..' or be an absolute path)", + }, + rules: [ + { + name: "posixPath", + params: { + options: Joi.object() + .keys({ + absoluteOnly: Joi.boolean() + .description("Only allow absolute paths (starting with /)."), + relativeOnly: Joi.boolean() + .description("Disallow absolute paths (starting with /)."), + subPathOnly: Joi.boolean() + .description("Only allow sub-paths. That is, disallow '..' path segments and absolute paths."), + }) + .oxor("absoluteOnly", "relativeOnly") + .oxor("absoluteOnly", "subPathOnly"), + }, + validate(params: { options?: JoiPathParams }, value: string, state, prefs) { + // Note: This relativeOnly param is in the context of URLs. + // Our own relativeOnly param is in the context of file paths. + const baseSchema = Joi.string().uri({ relativeOnly: true }) + const result = baseSchema.validate(value) + + if (result.error) { + // tslint:disable-next-line:no-invalid-this + return this.createError("posixPath", { v: value }, state, prefs) + } + + const options = params.options || {} + + if (options.absoluteOnly) { + if (!value.startsWith("/")) { + // tslint:disable-next-line:no-invalid-this + return this.createError("string.absoluteOnly", { v: value }, state, prefs) + } + } else if (options.subPathOnly) { + if (value.startsWith("/") || value.split("/").includes("..")) { + // tslint:disable-next-line:no-invalid-this + return this.createError("string.subPathOnly", { v: value }, state, prefs) + } + } else if (options.relativeOnly) { + if (value.startsWith("/")) { + // tslint:disable-next-line:no-invalid-this + return this.createError("string.relativeOnly", { v: value }, state, prefs) + } + } + + return value // Everything is OK + }, + }, + ], +}) + +export const joiPrimitive = () => joi.alternatives().try( + joi.number(), + joi.string().allow("").allow(null), + joi.boolean(), ).description("Number, string or boolean") export const absolutePathRegex = /^\/.*/ // Note: Only checks for the leading slash @@ -38,7 +114,7 @@ export const identifierRegex = /^(?![0-9]+$)(?!.*-$)(?!-)[a-z0-9-]{1,63}$/ export const userIdentifierRegex = /^(?!garden)(?=.{1,63}$)[a-z][a-z0-9]*(-[a-z0-9]+)*$/ export const envVarRegex = /^(?!garden)[a-z_][a-z0-9_]*$/i -export const joiIdentifier = () => Joi.string() +export const joiIdentifier = () => joi.string() .regex(identifierRegex) .description( "Valid RFC1035/RFC1123 (DNS) label (may contain lowercase letters, numbers and dashes, must start with a letter, " + @@ -53,7 +129,7 @@ export const joiProviderName = (name: string) => joiIdentifier().required() export const joiStringMap = (valueSchema: JoiObject) => Joi .object().pattern(/.+/, valueSchema) -export const joiUserIdentifier = () => Joi.string() +export const joiUserIdentifier = () => joi.string() .regex(userIdentifierRegex) .description( "Valid RFC1035/RFC1123 (DNS) label (may contain lowercase letters, numbers and dashes, must start with a letter, " + diff --git a/garden-service/src/config/config-context.ts b/garden-service/src/config/config-context.ts index 0e90f93ab2..3c628eaff0 100644 --- a/garden-service/src/config/config-context.ts +++ b/garden-service/src/config/config-context.ts @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import Joi = require("@hapi/joi") import username = require("username") import { isString } from "lodash" import { PrimitiveMap, isPrimitive, Primitive, joiIdentifierMap, joiStringMap, joiPrimitive } from "./common" @@ -13,9 +14,9 @@ import { Provider, ProviderConfig } from "./provider" import { ModuleConfig } from "./module" import { ConfigurationError } from "../exceptions" import { resolveTemplateString } from "../template-string" -import * as Joi from "joi" import { Garden } from "../garden" import { ModuleVersion } from "../vcs/vcs" +import { joi } from "../config/common" export type ContextKey = string[] @@ -49,7 +50,7 @@ export abstract class ConfigContext { static getSchema() { const schemas = (this)._schemas - return Joi.object().keys(schemas).required() + return joi.object().keys(schemas).required() } async resolve({ key, nodePath, opts }: ContextResolveParams): Promise { @@ -158,14 +159,14 @@ export abstract class ConfigContext { class LocalContext extends ConfigContext { @schema( - joiStringMap(Joi.string()).description( + joiStringMap(joi.string()).description( "A map of all local environment variables (see https://nodejs.org/api/process.html#process_process_env).", ), ) public env: typeof process.env @schema( - Joi.string() + joi.string() .description( "A string indicating the platform that the framework is running on " + "(see https://nodejs.org/api/process.html#process_process_platform)", @@ -175,7 +176,7 @@ class LocalContext extends ConfigContext { public platform: string @schema( - Joi.string() + joi.string() .description( "The current username (as resolved by https://github.com/sindresorhus/username)", ) @@ -212,7 +213,7 @@ export class ProjectConfigContext extends ConfigContext { class ProjectContext extends ConfigContext { @schema( - Joi.string() + joi.string() .description("The name of the Garden project.") .example("my-project"), ) @@ -226,7 +227,7 @@ class ProjectContext extends ConfigContext { class EnvironmentContext extends ConfigContext { @schema( - Joi.string() + joi.string() .description("The name of the environment Garden is running against.") .example("local"), ) @@ -242,7 +243,7 @@ const providersExample = { kubernetes: { config: { clusterHostname: "my-cluster. class ProviderContext extends ConfigContext { @schema( - Joi.object() + joi.object() .description("The resolved configuration for the provider.") .example(providersExample.kubernetes), ) @@ -300,7 +301,7 @@ const exampleVersion = "v-17ad4cb3fd" class ModuleContext extends ConfigContext { @schema( - Joi.string() + joi.string() .description("The build path of the module.") .example("/home/me/code/my-project/.garden/build/my-module"), ) @@ -316,10 +317,10 @@ class ModuleContext extends ConfigContext { ) public outputs: PrimitiveMap - @schema(Joi.string().description("The local path of the module.").example("/home/me/code/my-project/my-module")) + @schema(joi.string().description("The local path of the module.").example("/home/me/code/my-project/my-module")) public path: string - @schema(Joi.string().description("The current version of the module.").example(exampleVersion)) + @schema(joi.string().description("The current version of the module.").example(exampleVersion)) public version: string constructor(root: ConfigContext, moduleConfig: ModuleConfig, buildPath: string, version: ModuleVersion) { diff --git a/garden-service/src/config/dashboard.ts b/garden-service/src/config/dashboard.ts index 4ee71aa13b..a61b9bd10e 100644 --- a/garden-service/src/config/dashboard.ts +++ b/garden-service/src/config/dashboard.ts @@ -6,8 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import Joi = require("joi") -import { joiArray } from "./common" +import { joiArray, joi } from "./common" export interface DashboardPage { title: string @@ -18,20 +17,20 @@ export interface DashboardPage { // children: DashboardPage[] } -export const dashboardPageSchema = Joi.object() +export const dashboardPageSchema = joi.object() .keys({ - title: Joi.string() + title: joi.string() .max(32) .required() .description("The link title to show in the menu bar (max length 32)."), - description: Joi.string() + description: joi.string() .required() .description("A description to show when hovering over the link."), - url: Joi.string() + url: joi.string() .uri() .required() .description("The URL to open in the dashboard pane when clicking the link."), - newWindow: Joi.boolean() + newWindow: joi.boolean() .default(false) .description("Set to true if the link should open in a new browser tab/window."), }) diff --git a/garden-service/src/config/module.ts b/garden-service/src/config/module.ts index 6d54537356..e2126289a7 100644 --- a/garden-service/src/config/module.ts +++ b/garden-service/src/config/module.ts @@ -8,7 +8,6 @@ import dedent = require("dedent") import stableStringify = require("json-stable-stringify") -import * as Joi from "joi" import { ServiceConfig, ServiceSpec, serviceConfigSchema } from "./service" import { joiArray, @@ -18,6 +17,7 @@ import { PrimitiveMap, joiIdentifierMap, joiPrimitive, + joi, } from "./common" import { TestConfig, TestSpec, testConfigSchema } from "./test" import { TaskConfig, TaskSpec, taskConfigSchema } from "./task" @@ -29,16 +29,15 @@ export interface BuildCopySpec { } // TODO: allow : delimited string (e.g. some.file:some-dir/) -const copySchema = Joi.object() +const copySchema = joi.object() .keys({ // TODO: allow array of strings here - // TODO: disallow paths outside of the module root - source: Joi.string() - .uri({ relativeOnly: true }) + source: joi.string() + .posixPath({ subPathOnly: true }) .required() .description("POSIX-style path or filename of the directory or file(s) to copy to the target."), - target: Joi.string() - .uri({ relativeOnly: true }) + target: joi.string() + .posixPath({ subPathOnly: true }) .default(() => "", "") .description( "POSIX-style path or filename to copy the directory or file(s).", @@ -53,7 +52,7 @@ export interface BuildDependencyConfig { copy: BuildCopySpec[] } -export const buildDependencySchema = Joi.object().keys({ +export const buildDependencySchema = joi.object().keys({ name: joiIdentifier().required() .description("Module name to build ahead of this module."), plugin: joiIdentifier() @@ -81,7 +80,7 @@ export interface BaseModuleSpec { repositoryUrl?: string } -export const baseBuildSpecSchema = Joi.object() +export const baseBuildSpecSchema = joi.object() .keys({ dependencies: joiArray(buildDependencySchema) .description("A list of modules that must be built before this module is built.") @@ -93,13 +92,13 @@ export const baseBuildSpecSchema = Joi.object() .default(() => ({ dependencies: [] }), "{}") .description("Specify how to build the module. Note that plugins may define additional keys on this object.") -export const baseModuleSpecSchema = Joi.object() +export const baseModuleSpecSchema = joi.object() .keys({ - apiVersion: Joi.string() + apiVersion: joi.string() .default(DEFAULT_API_VERSION) .only(DEFAULT_API_VERSION) .description("The schema version of this module's config (currently not used)."), - kind: Joi.string().default("Module").only("Module"), + kind: joi.string().default("Module").only("Module"), type: joiIdentifier() .required() .description("The type of this module.") @@ -108,8 +107,8 @@ export const baseModuleSpecSchema = Joi.object() .required() .description("The name of this module.") .example("my-sweet-module"), - description: Joi.string(), - include: Joi.array().items(Joi.string().uri({ relativeOnly: true })) + description: joi.string(), + include: joi.array().items(joi.string().posixPath({ subPathOnly: true })) .description( dedent`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, @@ -127,7 +126,7 @@ export const baseModuleSpecSchema = Joi.object() Garden will import the repository source code into this module, but read the module's config from the local garden.yml file.`, ), - allowPublish: Joi.boolean() + allowPublish: joi.boolean() .default(true) .description("When false, disables pushing this module to remote registries."), build: baseBuildSpecSchema @@ -162,7 +161,7 @@ export const moduleConfigSchema = baseModuleSpecSchema .keys({ outputs: joiIdentifierMap(joiPrimitive()) .description("The outputs defined by the module (referenceable in other module configs)."), - path: Joi.string().uri({ relativeOnly: true }) + path: joi.string() .description("The filesystem path of the module."), plugin: joiIdentifier() .meta({ internal: true }) @@ -173,10 +172,10 @@ export const moduleConfigSchema = baseModuleSpecSchema .description("List of tasks configured by this module."), testConfigs: joiArray(testConfigSchema) .description("List of tests configured by this module."), - spec: Joi.object() + spec: joi.object() .meta({ extendable: true }) .description("The module spec, as defined by the provider plugin."), - _ConfigType: Joi.object() + _ConfigType: joi.object() .meta({ internal: true }), }) .description("The configuration for a module.") diff --git a/garden-service/src/config/project.ts b/garden-service/src/config/project.ts index 642c9d0c2e..b831d81cbf 100644 --- a/garden-service/src/config/project.ts +++ b/garden-service/src/config/project.ts @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { safeDump } from "js-yaml" import { apply, merge } from "json-merge-patch" import { deline } from "../util/string" @@ -18,6 +17,7 @@ import { joiRepositoryUrl, joiUserIdentifier, validateWithPath, + joi, } from "./common" import { resolveTemplateStrings } from "../template-string" import { ProjectConfigContext } from "./config-context" @@ -34,7 +34,7 @@ export interface CommonEnvironmentConfig { variables: { [key: string]: Primitive } } -export const environmentConfigSchema = Joi.object() +export const environmentConfigSchema = joi.object() .keys({ providers: joiArray(providerConfigBaseSchema) .unique("name") @@ -67,10 +67,10 @@ const environmentSchema = environmentConfigSchema name: environmentNameSchema, }) -const environmentsSchema = Joi.alternatives( - Joi.array().items(environmentSchema).unique("name"), +const environmentsSchema = joi.alternatives( + joi.array().items(environmentSchema).unique("name"), // Allow a string as a shorthand for { name: foo } - Joi.array().items(joiUserIdentifier()), + joi.array().items(joiUserIdentifier()), ) export interface SourceConfig { @@ -78,7 +78,7 @@ export interface SourceConfig { repositoryUrl: string } -export const projectSourceSchema = Joi.object() +export const projectSourceSchema = joi.object() .keys({ name: joiUserIdentifier() .required() @@ -131,16 +131,16 @@ export const projectNameSchema = joiIdentifier() .description("The name of the project.") .example("my-sweet-project") -export const projectSchema = Joi.object() +export const projectSchema = joi.object() .keys({ - apiVersion: Joi.string() + apiVersion: joi.string() .default(DEFAULT_API_VERSION) .only(DEFAULT_API_VERSION) .description("The schema version of this project's config (currently not used)."), - kind: Joi.string().default("Project").only("Project"), - path: Joi.string().meta({ internal: true }), + kind: joi.string().default("Project").only("Project"), + path: joi.string().meta({ internal: true }), name: projectNameSchema, - defaultEnvironment: Joi.string() + defaultEnvironment: joi.string() .allow("") .default("", "") .description("The default environment to use when calling commands without the `--env` parameter."), diff --git a/garden-service/src/config/provider.ts b/garden-service/src/config/provider.ts index 9b089665b8..a4b89f918b 100644 --- a/garden-service/src/config/provider.ts +++ b/garden-service/src/config/provider.ts @@ -6,9 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { deline } from "../util/string" -import { joiIdentifier, joiUserIdentifier, joiArray } from "./common" +import { joiIdentifier, joiUserIdentifier, joiArray, joi } from "./common" import { collectTemplateReferences } from "../template-string" import { ConfigurationError } from "../exceptions" import { ModuleConfig, moduleConfigSchema } from "./module" @@ -21,13 +20,13 @@ export interface ProviderConfig { [key: string]: any } -const providerFixedFieldsSchema = Joi.object() +const providerFixedFieldsSchema = joi.object() .keys({ name: joiIdentifier() .required() .description("The name of the provider plugin to use.") .example("local-kubernetes"), - environments: Joi.array().items(joiUserIdentifier()) + environments: joi.array().items(joiUserIdentifier()) .optional() .description(deline` If specified, this provider will only be used in the listed environments. Note that an empty array effectively @@ -50,9 +49,9 @@ export interface Provider { export const providerSchema = providerFixedFieldsSchema .keys({ - dependencies: Joi.lazy(() => providersSchema) + dependencies: joi.lazy(() => providersSchema) .required(), - config: Joi.lazy(() => providerConfigBaseSchema) + config: joi.lazy(() => providerConfigBaseSchema) .required(), moduleConfigs: joiArray(moduleConfigSchema.optional()), }) diff --git a/garden-service/src/config/service.ts b/garden-service/src/config/service.ts index 9e4500ebc3..16cc2bd383 100644 --- a/garden-service/src/config/service.ts +++ b/garden-service/src/config/service.ts @@ -7,8 +7,7 @@ */ import deline = require("deline") -import * as Joi from "joi" -import { joiIdentifier, joiIdentifierMap, joiPrimitive, joiArray, joiUserIdentifier } from "./common" +import { joiIdentifier, joiIdentifierMap, joiPrimitive, joiArray, joiUserIdentifier, joi } from "./common" export interface ServiceSpec { } @@ -23,7 +22,7 @@ export interface CommonServiceSpec extends ServiceSpec { export const serviceOutputsSchema = joiIdentifierMap(joiPrimitive()) -export const baseServiceSpecSchema = Joi.object() +export const baseServiceSpecSchema = joi.object() .keys({ name: joiUserIdentifier().required(), dependencies: joiArray(joiIdentifier()) @@ -46,7 +45,7 @@ export interface ServiceConfig extends Comm export const serviceConfigSchema = baseServiceSpecSchema .keys({ - hotReloadable: Joi.boolean() + hotReloadable: joi.boolean() .default(false) .description("Set this to true if the module and service configuration supports hot reloading."), sourceModuleName: joiIdentifier() @@ -56,7 +55,7 @@ export const serviceConfigSchema = baseServiceSpecSchema 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). `), - spec: Joi.object() + spec: joi.object() .meta({ extendable: true }) .description("The service's specification, as defined by its provider plugin."), }) diff --git a/garden-service/src/config/task.ts b/garden-service/src/config/task.ts index bab25fb286..33d391ffa1 100644 --- a/garden-service/src/config/task.ts +++ b/garden-service/src/config/task.ts @@ -7,8 +7,7 @@ */ import deline = require("deline") -import * as Joi from "joi" -import { joiArray, joiUserIdentifier } from "./common" +import { joiArray, joiUserIdentifier, joi } from "./common" export interface TaskSpec { } @@ -19,19 +18,19 @@ export interface BaseTaskSpec extends TaskSpec { timeout: number | null } -export const baseTaskSpecSchema = Joi.object() +export const baseTaskSpecSchema = joi.object() .keys({ name: joiUserIdentifier() .required() .description("The name of the task."), - description: Joi.string().optional() + description: joi.string().optional() .description("A description of the task."), - dependencies: joiArray(Joi.string()) + dependencies: joiArray(joi.string()) .description(deline` The names of any tasks that must be executed, and the names of any services that must be running, before this task is executed. `), - timeout: Joi.number() + timeout: joi.number() .optional() .allow(null) .default(null) @@ -46,22 +45,22 @@ export interface TaskConfig extends BaseTaskSpec export const taskConfigSchema = baseTaskSpecSchema .keys({ - spec: Joi.object() + spec: joi.object() .meta({ extendable: true }) .description("The task's specification, as defined by its provider plugin."), }) .description("The configuration for a module's task.") -export const taskSchema = Joi.object() +export const taskSchema = joi.object() .options({ presence: "required" }) .keys({ name: joiUserIdentifier() .description("The name of the task."), - description: Joi.string().optional() + description: joi.string().optional() .description("A description of the task."), - module: Joi.object().unknown(true), + module: joi.object().unknown(true), config: taskConfigSchema, - spec: Joi.object() + spec: joi.object() .meta({ extendable: true }) .description("The configuration of the task (specific to each plugin)."), }) diff --git a/garden-service/src/config/test.ts b/garden-service/src/config/test.ts index 75c6035186..b03a2c851f 100644 --- a/garden-service/src/config/test.ts +++ b/garden-service/src/config/test.ts @@ -7,10 +7,10 @@ */ import deline = require("deline") -import * as Joi from "joi" import { joiArray, joiUserIdentifier, + joi, } from "./common" export interface TestSpec { } @@ -21,17 +21,17 @@ export interface BaseTestSpec extends TestSpec { timeout: number | null } -export const baseTestSpecSchema = Joi.object() +export const baseTestSpecSchema = joi.object() .keys({ name: joiUserIdentifier() .required() .description("The name of the test."), - dependencies: joiArray(Joi.string()) + dependencies: joiArray(joi.string()) .description(deline` The names of any services that must be running, and the names of any tasks that must be executed, before the test is run. `), - timeout: Joi.number() + timeout: joi.number() .allow(null) .default(null) .description("Maximum duration (in seconds) of the test run."), @@ -44,7 +44,7 @@ export interface TestConfig extends BaseTestSpec export const testConfigSchema = baseTestSpecSchema .keys({ - spec: Joi.object() + spec: joi.object() .meta({ extendable: true }) .description("The configuration for the test, as specified by its module's provider."), }) diff --git a/garden-service/src/docs/config.ts b/garden-service/src/docs/config.ts index ff92122035..efb2b2dce3 100644 --- a/garden-service/src/docs/config.ts +++ b/garden-service/src/docs/config.ts @@ -6,13 +6,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import Joi = require("@hapi/joi") import { readFileSync, writeFileSync, } from "fs" import { safeDump } from "js-yaml" import * as linewrap from "linewrap" -import * as Joi from "joi" import { resolve } from "path" import { get, @@ -26,7 +26,7 @@ import handlebars = require("handlebars") import { configSchema as localK8sConfigSchema } from "../plugins/kubernetes/local/config" import { configSchema as k8sConfigSchema } from "../plugins/kubernetes/config" import { configSchema as openfaasConfigSchema } from "../plugins/openfaas/openfaas" -import { joiArray } from "../config/common" +import { joiArray, joi } from "../config/common" import { mavenContainerConfigSchema } from "../plugins/maven-container/maven-container" import { Garden } from "../garden" import { GARDEN_SERVICE_ROOT } from "../constants" @@ -37,7 +37,7 @@ export const TEMPLATES_DIR = resolve(GARDEN_SERVICE_ROOT, "src", "docs", "templa const populateModuleSchema = (schema: Joi.ObjectSchema) => baseModuleSpecSchema .concat(schema) -const populateProviderSchema = (schema: Joi.ObjectSchema) => Joi.object() +const populateProviderSchema = (schema: Joi.ObjectSchema) => joi.object() .keys({ providers: joiArray(schema), }) diff --git a/garden-service/src/plugin-context.ts b/garden-service/src/plugin-context.ts index 666aad3a83..7b5361b179 100644 --- a/garden-service/src/plugin-context.ts +++ b/garden-service/src/plugin-context.ts @@ -8,12 +8,12 @@ import { Garden } from "./garden" import { keyBy, cloneDeep } from "lodash" -import * as Joi from "joi" import { projectNameSchema, projectSourcesSchema, environmentNameSchema } from "./config/project" import { PluginError } from "./exceptions" import { defaultProvider, Provider, providerSchema, ProviderConfig } from "./config/provider" import { configStoreSchema } from "./config-store" import { deline } from "./util/string" +import { joi } from "./config/common" type WrappedFromGarden = Pick extend // NOTE: this is used more for documentation than validation, outside of internal testing // TODO: validate the output from createPluginContext against this schema (in tests) -export const pluginContextSchema = Joi.object() +export const pluginContextSchema = joi.object() .options({ presence: "required" }) .keys({ projectName: projectNameSchema, - projectRoot: Joi.string() + projectRoot: joi.string() .description("The absolute path of the project root."), - gardenDirPath: Joi.string() + gardenDirPath: joi.string() .description(deline` The absolute path of the project's Garden dir. This is the directory the contains builds, logs and other meta data. A custom path can be set when initialising the Garden class. Defaults to \`.garden\`. @@ -48,7 +48,7 @@ export const pluginContextSchema = Joi.object() environmentName: environmentNameSchema, provider: providerSchema .description("The provider being used for this context."), - workingCopyId: Joi.string() + workingCopyId: joi.string() .description("A unique ID assigned to the current project working copy."), }) diff --git a/garden-service/src/plugins/container/config.ts b/garden-service/src/plugins/container/config.ts index eaf932cf9c..de186e8459 100644 --- a/garden-service/src/plugins/container/config.ts +++ b/garden-service/src/plugins/container/config.ts @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import deline = require("deline") import { Module, FileCopySpec } from "../../types/module" @@ -16,7 +15,7 @@ import { joiArray, PrimitiveMap, joiPrimitive, - absolutePathRegex, + joi, } from "../../config/common" import { Service, ingressHostnameSchema } from "../../types/service" import { DEFAULT_PORT_PROTOCOL } from "../../constants" @@ -93,16 +92,17 @@ export interface ContainerServiceSpec extends CommonServiceSpec { const commandExample = ["/bin/sh", "-c"] -const hotReloadSyncSchema = Joi.object() +const hotReloadSyncSchema = joi.object() .keys({ - source: Joi.string().uri({ relativeOnly: true }) + source: joi.string() + .posixPath({ subPathOnly: true }) .default(".") .description(deline` POSIX-style path of the directory to sync to the target, relative to the module's top-level directory. Must be a relative path if provided. Defaults to the module's top-level directory if no value is provided.`) .example("src"), - target: Joi.string().uri({ relativeOnly: true }) - .regex(absolutePathRegex) + target: joi.string() + .posixPath({ absoluteOnly: true }) .required() .description(deline` POSIX-style absolute path to sync the directory to inside the container. The root path (i.e. "/") is @@ -114,9 +114,9 @@ export interface ContainerHotReloadSpec { sync: FileCopySpec[] } -const hotReloadConfigSchema = Joi.object() +const hotReloadConfigSchema = joi.object() .keys({ - sync: Joi.array().items(hotReloadSyncSchema) + sync: joi.array().items(hotReloadSyncSchema) .required() .description( "Specify one or more source files or directories to automatically sync into the running container.", @@ -130,64 +130,65 @@ const hotReloadConfigSchema = Joi.object() export type ContainerServiceConfig = ServiceConfig -const annotationsSchema = joiStringMap(Joi.string()) +const annotationsSchema = joiStringMap(joi.string()) .default(() => ({}), "{}") -const ingressSchema = Joi.object() +const ingressSchema = joi.object() .keys({ annotations: annotationsSchema .description("Annotations to attach to the ingress (Note: May not be applicable to all providers)"), hostname: ingressHostnameSchema, - path: Joi.string().uri({ relativeOnly: true }) + path: joi.string() + .uri({ relativeOnly: true }) .default("/") .description("The path which should be routed to the service."), - port: Joi.string() + port: joi.string() .required() .description("The name of the container port where the specified paths should be routed."), }) -const healthCheckSchema = Joi.object() +const healthCheckSchema = joi.object() .keys({ - httpGet: Joi.object() + httpGet: joi.object() .keys({ - path: Joi.string() + path: joi.string() .uri({ relativeOnly: true }) .required() .description("The path of the service's health check endpoint."), - port: Joi.string() + port: joi.string() .required() .description("The name of the port where the service's health check endpoint should be available."), - scheme: Joi.string().allow("HTTP", "HTTPS").default("HTTP"), + scheme: joi.string().allow("HTTP", "HTTPS").default("HTTP"), }) .description("Set this to check the service's health by making an HTTP request."), - command: Joi.array().items(Joi.string()) + command: joi.array().items(joi.string()) .description("Set this to check the service's health by running a command in its container."), - tcpPort: Joi.string() + tcpPort: joi.string() .description("Set this to check the service's health by checking if this TCP port is accepting connections."), }).xor("httpGet", "command", "tcpPort") -const limitsSchema = Joi.object() +const limitsSchema = joi.object() .keys({ - cpu: Joi.number() + cpu: joi.number() .default(defaultContainerLimits.cpu) .min(10) .description("The maximum amount of CPU the service can use, in millicpus (i.e. 1000 = 1 CPU)"), - memory: Joi.number() + memory: joi.number() .default(defaultContainerLimits.memory) .min(64) .description("The maximum amount of RAM the service can use, in megabytes (i.e. 1024 = 1 GB)"), }) -export const portSchema = Joi.object() +export const portSchema = joi.object() .keys({ name: joiUserIdentifier() .required() .description("The name of the port (used when referencing the port elsewhere in the service configuration)."), - protocol: Joi.string() + protocol: joi.string() .allow("TCP", "UDP") .default(DEFAULT_PORT_PROTOCOL) .description("The protocol of the port."), - containerPort: Joi.number() + containerPort: joi.number() .required() .example("8080") .description(deline` @@ -195,30 +196,30 @@ export const portSchema = Joi.object() for \`servicePort\`. \`servicePort:80 -> containerPort:8080 -> process:8080\``), - servicePort: Joi.number() + servicePort: joi.number() .default((context) => context.containerPort, "") .example("80") .description(deline`The port exposed on the service. Defaults to \`containerPort\` if not specified. \`servicePort:80 -> containerPort:8080 -> process:8080\``), - hostPort: Joi.number() + hostPort: joi.number() .meta({ deprecated: true }), - nodePort: Joi.number() + nodePort: joi.number() .description(deline` Set this to expose the service on the specified port on the host node (may not be supported by all providers).`), }) -const volumeSchema = Joi.object() +const volumeSchema = joi.object() .keys({ name: joiUserIdentifier() .required() .description("The name of the allocated volume."), - containerPath: Joi.string() + containerPath: joi.string() .required() .description("The path where the volume should be mounted in the container."), - hostPath: Joi.string() + hostPath: joi.string() .meta({ deprecated: true }), }) @@ -226,13 +227,13 @@ const serviceSchema = baseServiceSpecSchema .keys({ annotations: annotationsSchema .description("Annotations to attach to the service (Note: May not be applicable to all providers)."), - command: Joi.array().items(Joi.string()) + command: joi.array().items(joi.string()) .description("The command/entrypoint to run the container with when starting the service.") .example([commandExample, {}]), - args: Joi.array().items(Joi.string()) + args: joi.array().items(joi.string()) .description("The arguments to run the container with when starting the service.") .example([["npm", "start"], {}]), - daemon: Joi.boolean() + daemon: joi.boolean() .default(false) .description(deline` Whether to run the service as a daemon (to ensure exactly one instance runs per node). @@ -247,12 +248,12 @@ const serviceSchema = baseServiceSpecSchema env: joiEnvVars(), healthCheck: healthCheckSchema .description("Specify how the service's health should be checked after deploying."), - hotReloadCommand: Joi.array().items(Joi.string()) + hotReloadCommand: joi.array().items(joi.string()) .description(deline` If this module uses the \`hotReload\` field, the container will be run with this command/entrypoint when the service is deployed with hot reloading enabled.`) .example([commandExample, {}]), - hotReloadArgs: Joi.array().items(Joi.string()) + hotReloadArgs: joi.array().items(joi.string()) .description(deline` If this module uses the \`hotReload\` field, the container will be run with these arguments when the service is deployed with hot reloading enabled.`) @@ -263,7 +264,7 @@ const serviceSchema = baseServiceSpecSchema ports: joiArray(portSchema) .unique("name") .description("List of ports that the service container exposes."), - replicas: Joi.number() + replicas: joi.number() .integer() .min(1) .default(1) @@ -284,16 +285,16 @@ export interface ContainerRegistryConfig { namespace: string, } -export const containerRegistryConfigSchema = Joi.object() +export const containerRegistryConfigSchema = joi.object() .keys({ - hostname: Joi.string() + hostname: joi.string() .required() .description("The hostname (and optionally port, if not the default port) of the registry.") .example("gcr.io"), - port: Joi.number() + port: joi.number() .integer() .description("The port where the registry listens on, if not the default."), - namespace: Joi.string() + namespace: joi.string() .default("_") .description("The namespace in the registry where images should be pushed.") .example("my-project"), @@ -313,10 +314,10 @@ export interface ContainerTestSpec extends BaseTestSpec { export const containerTestSchema = baseTestSpecSchema .keys({ - command: Joi.array().items(Joi.string()) + command: joi.array().items(joi.string()) .description("The command/entrypoint used to run the test inside the container.") .example([commandExample, {}]), - args: Joi.array().items(Joi.string()) + args: joi.array().items(joi.string()) .description("The arguments used to run the test inside the container.") .example([["npm", "test"], {}]), env: joiEnvVars(), @@ -330,10 +331,10 @@ export interface ContainerTaskSpec extends BaseTaskSpec { export const containerTaskSchema = baseTaskSpecSchema .keys({ - command: Joi.array().items(Joi.string()) + command: joi.array().items(joi.string()) .description("The command/entrypoint used to run the task inside the container.") .example([commandExample, {}]), - args: Joi.array().items(Joi.string()) + args: joi.array().items(joi.string()) .description("The arguments used to run the task inside the container.") .example([["rake", "db:migrate"], {}]), env: joiEnvVars(), @@ -360,30 +361,31 @@ export interface ContainerModuleConfig extends ModuleConfig export const defaultNamespace = "_" export const defaultTag = "latest" -export const containerModuleSpecSchema = Joi.object() +export const containerModuleSpecSchema = joi.object() .keys({ build: baseBuildSpecSchema .keys({ - targetImage: Joi.string() + targetImage: joi.string() .description(deline` For multi-stage Dockerfiles, specify which image to build (see https://docs.docker.com/engine/reference/commandline/build/#specifying-target-build-stage---target for details). `), }), - buildArgs: Joi.object() + buildArgs: joi.object() .pattern(/.+/, joiPrimitive()) .default(() => ({}), "{}") .description("Specify build arguments to use when building the container image."), // TODO: validate the image name format - image: Joi.string() + image: joi.string() .description(deline` Specify the image name for the container. Should be a valid Docker image identifier. If specified and the module does not contain a Dockerfile, this image will be used to deploy services for this module. If specified and the module does contain a Dockerfile, this identifier is used when pushing the built image.`), hotReload: hotReloadConfigSchema, - dockerfile: Joi.string().uri({ relativeOnly: true }) - .description("POSIX-style name of Dockerfile, relative to project root. Defaults to $MODULE_ROOT/Dockerfile."), + dockerfile: joi.string() + .posixPath({ subPathOnly: true }) + .description("POSIX-style name of Dockerfile, relative to module root."), services: joiArray(serviceSchema) .unique("name") .description("The list of services to deploy from this container module."), diff --git a/garden-service/src/plugins/container/container.ts b/garden-service/src/plugins/container/container.ts index 233e686860..b3e6fcaece 100644 --- a/garden-service/src/plugins/container/container.ts +++ b/garden-service/src/plugins/container/container.ts @@ -7,7 +7,6 @@ */ import dedent = require("dedent") -import * as Joi from "joi" import { keyBy } from "lodash" import { ConfigurationError } from "../../exceptions" @@ -19,15 +18,16 @@ import { KubernetesProvider } from "../kubernetes/config" import { ConfigureModuleParams } from "../../types/plugin/module/configure" import { PublishModuleParams } from "../../types/plugin/module/publishModule" import { HotReloadServiceParams } from "../../types/plugin/service/hotReloadService" +import { joi } from "../../config/common" -export const containerModuleOutputsSchema = Joi.object() +export const containerModuleOutputsSchema = joi.object() .keys({ - "local-image-name": Joi.string() + "local-image-name": joi.string() .required() .description( "The name of the image (without tag/version) that the module uses for local builds and deployments.", ), - "deployment-image-name": Joi.string() + "deployment-image-name": joi.string() .required() .description("The name of the image (without tag/version) that the module will use during deployment."), }) diff --git a/garden-service/src/plugins/exec.ts b/garden-service/src/plugins/exec.ts index b5e795560a..0eb461c486 100644 --- a/garden-service/src/plugins/exec.ts +++ b/garden-service/src/plugins/exec.ts @@ -6,10 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { mapValues } from "lodash" import { join } from "path" -import { joiArray, joiEnvVars, validateWithPath } from "../config/common" +import { joiArray, joiEnvVars, validateWithPath, joi } from "../config/common" import { GardenPlugin } from "../types/plugin/plugin" import { Module } from "../types/module" import { CommonServiceSpec } from "../config/service" @@ -36,7 +35,7 @@ export interface ExecTestSpec extends BaseTestSpec { export const execTestSchema = baseTestSpecSchema .keys({ - command: Joi.array().items(Joi.string()) + command: joi.array().items(joi.string()) .description("The command to run in the module build context in order to test it."), env: joiEnvVars(), }) @@ -48,7 +47,7 @@ export interface ExecTaskSpec extends BaseTaskSpec { export const execTaskSpecSchema = baseTaskSpecSchema .keys({ - command: Joi.array().items(Joi.string()) + command: joi.array().items(joi.string()) .description("The command to run in the module build context."), }) .description("A task that can be run in this module.") @@ -68,12 +67,12 @@ export type ExecModuleConfig = ModuleConfig export const execBuildSpecSchema = baseBuildSpecSchema .keys({ - command: joiArray(Joi.string()) + command: joiArray(joi.string()) .description("The command to run inside the module's directory to perform the build.") .example([["npm", "run", "build"], {}]), }) -export const execModuleSpecSchema = Joi.object() +export const execModuleSpecSchema = joi.object() .keys({ build: execBuildSpecSchema, env: joiEnvVars(), @@ -230,7 +229,7 @@ async function describeType() { A simple module for executing commands in your shell. This can be a useful escape hatch if no other module type fits your needs, and you just need to execute something (as opposed to deploy it, track its status etc.). `, - outputsSchema: Joi.object().keys({}), + outputsSchema: joi.object().keys({}), schema: execModuleSpecSchema, } } diff --git a/garden-service/src/plugins/google/google-app-engine.ts b/garden-service/src/plugins/google/google-app-engine.ts index 41d30b5db9..96a4070a18 100644 --- a/garden-service/src/plugins/google/google-app-engine.ts +++ b/garden-service/src/plugins/google/google-app-engine.ts @@ -19,12 +19,12 @@ import { GardenPlugin } from "../../types/plugin/plugin" import { configureContainerModule } from "../container/container" import { ContainerModule } from "../container/config" import { providerConfigBaseSchema } from "../../config/provider" -import * as Joi from "joi" import { ConfigureModuleParams } from "../../types/plugin/module/configure" import { DeployServiceParams } from "../../types/plugin/service/deployService" +import { joi } from "../../config/common" const configSchema = providerConfigBaseSchema.keys({ - project: Joi.string() + project: joi.string() .required() .description("The GCP project to deploy containers to."), }) diff --git a/garden-service/src/plugins/google/google-cloud-functions.ts b/garden-service/src/plugins/google/google-cloud-functions.ts index 99b9b2ec76..9402026d8e 100644 --- a/garden-service/src/plugins/google/google-cloud-functions.ts +++ b/garden-service/src/plugins/google/google-cloud-functions.ts @@ -6,11 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { joiArray, validateWithPath } from "../../config/common" +import { joiArray, validateWithPath, joi } from "../../config/common" import { Module } from "../../types/module" import { ServiceState, ServiceStatus, ingressHostnameSchema, Service } from "../../types/service" import { resolve } from "path" -import * as Joi from "joi" import { ExecTestSpec, execTestSchema } from "../exec" import { prepareEnvironment, @@ -29,13 +28,13 @@ import { gardenAnnotationKey } from "../../util/string" const gcfModuleSpecSchema = baseServiceSpecSchema .keys({ - entrypoint: Joi.string() + entrypoint: joi.string() .description("The entrypoint for the function (exported name in the function's module)"), hostname: ingressHostnameSchema, - path: Joi.string() + path: joi.string() .default(".") .description("The path of the module that contains the function."), - project: Joi.string() + project: joi.string() .description("The Google Cloud project name of the function."), tests: joiArray(execTestSchema), }) @@ -97,7 +96,7 @@ export async function configureGcfModule( } const configSchema = providerConfigBaseSchema.keys({ - project: Joi.string() + project: joi.string() .description("The default GCP project to deploy functions to (can be overridden on individual functions)."), }) diff --git a/garden-service/src/plugins/kubernetes/config.ts b/garden-service/src/plugins/kubernetes/config.ts index f14affee34..259b520a95 100644 --- a/garden-service/src/plugins/kubernetes/config.ts +++ b/garden-service/src/plugins/kubernetes/config.ts @@ -6,10 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import dedent = require("dedent") -import { joiArray, joiIdentifier, joiProviderName } from "../../config/common" +import { joiArray, joiIdentifier, joiProviderName, joi } from "../../config/common" import { Provider, providerConfigBaseSchema, ProviderConfig } from "../../config/provider" import { containerRegistryConfigSchema, ContainerRegistryConfig } from "../container/config" import { PluginContext } from "../../plugin-context" @@ -130,30 +129,30 @@ export const defaultStorage: KubernetesStorage = { }, } -const resourceSchema = (defaults: KubernetesResourceSpec) => Joi.object() +const resourceSchema = (defaults: KubernetesResourceSpec) => joi.object() .keys({ - limits: Joi.object() + limits: joi.object() .keys({ - cpu: Joi.number() + cpu: joi.number() .integer() .default(defaults.limits.cpu) .description("CPU limit in millicpu.") .example(defaults.limits.cpu), - memory: Joi.number() + memory: joi.number() .integer() .default(defaults.limits.memory) .description("Memory limit in megabytes.") .example(defaults.limits.memory), }) .default(defaults.limits), - requests: Joi.object() + requests: joi.object() .keys({ - cpu: Joi.number() + cpu: joi.number() .integer() .default(defaults.requests.cpu) .description("CPU request in millicpu.") .example(defaults.requests.cpu), - memory: Joi.number() + memory: joi.number() .integer() .default(defaults.requests.memory) .description("Memory request in megabytes.") @@ -163,25 +162,25 @@ const resourceSchema = (defaults: KubernetesResourceSpec) => Joi.object() }) .default(defaults) -const storageSchema = (defaults: KubernetesStorageSpec) => Joi.object() +const storageSchema = (defaults: KubernetesStorageSpec) => joi.object() .keys({ - size: Joi.number() + size: joi.number() .integer() .default(defaults.size) .description("Volume size for the registry in megabytes."), - storageClass: Joi.string() + storageClass: joi.string() .allow(null) .default(null) .description("Storage class to use for the volume."), }) .default(defaults) -export const k8sContextSchema = Joi.string() +export const k8sContextSchema = joi.string() .required() .description("The kubectl context to use to connect to the Kubernetes cluster.") .example("my-dev-context") -const secretRef = Joi.object() +const secretRef = joi.object() .keys({ name: joiIdentifier() .required() @@ -203,14 +202,14 @@ const imagePullSecretsSchema = joiArray(secretRef) when configuring a remote Kubernetes environment with buildMode=local. `) -const tlsCertificateSchema = Joi.object() +const tlsCertificateSchema = joi.object() .keys({ name: joiIdentifier() .required() .description("A unique identifier for this certificate.") .example("www") .example("wildcard"), - hostnames: Joi.array().items(Joi.string().hostname()) + hostnames: joi.array().items(joi.string().hostname()) .description( "A list of hostnames that this certificate should be used for. " + "If you don't specify these, they will be automatically read from the certificate.", @@ -223,7 +222,7 @@ const tlsCertificateSchema = Joi.object() export const kubernetesConfigBase = providerConfigBaseSchema .keys({ - buildMode: Joi.string() + buildMode: joi.string() .allow("local-docker", "cluster-docker", "kaniko") .default("local-docker") .description(dedent` @@ -243,19 +242,19 @@ export const kubernetesConfigBase = providerConfigBaseSchema this is less secure than Kaniko, but in turn it is generally faster. See the [Kaniko docs](https://github.com/GoogleContainerTools/kaniko) for more information on Kaniko. `), - defaultHostname: Joi.string() + defaultHostname: joi.string() .description("A default hostname to use when no hostname is explicitly configured for a service.") .example("api.mydomain.com"), defaultUsername: joiIdentifier() .description("Set a default username (used for namespacing within a cluster)."), - forceSsl: Joi.boolean() + forceSsl: joi.boolean() .default(false) .description( "Require SSL on all `container` module services. If set to true, an error is raised when no certificate " + "is available for a configured hostname on a `container` module.", ), imagePullSecrets: imagePullSecretsSchema, - resources: Joi.object() + resources: joi.object() .keys({ builder: resourceSchema(defaultResources.builder) .description(dedent` @@ -288,7 +287,7 @@ export const kubernetesConfigBase = providerConfigBaseSchema Resource requests and limits for the in-cluster builder, container registry and code sync service. (which are automatically installed and used when \`buildMode\` is \`cluster-docker\` or \`kaniko\`). `), - storage: Joi.object() + storage: joi.object() .keys({ builder: storageSchema(defaultStorage.builder) .description(dedent` @@ -332,24 +331,24 @@ export const configSchema = kubernetesConfigBase context: k8sContextSchema .required(), deploymentRegistry: containerRegistryConfigSchema, - ingressClass: Joi.string() + ingressClass: joi.string() .description(dedent` The ingress class to use on configured Ingresses (via the \`kubernetes.io/ingress.class\` annotation) when deploying \`container\` services. Use this if you have multiple ingress controllers in your cluster. `), - ingressHttpPort: Joi.number() + ingressHttpPort: joi.number() .default(80) .description("The external HTTP port of the cluster's ingress controller."), - ingressHttpsPort: Joi.number() + ingressHttpsPort: joi.number() .default(443) .description("The external HTTPS port of the cluster's ingress controller."), - namespace: Joi.string() + namespace: joi.string() .default(undefined, "") .description( "Specify which namespace to deploy services to (defaults to ). " + "Note that the framework generates other namespaces as well with this name as a prefix.", ), - setupIngressController: Joi.string() + setupIngressController: joi.string() .allow("nginx", false, null) .default(false) .description("Set this to `nginx` to install/enable the NGINX ingress controller."), diff --git a/garden-service/src/plugins/kubernetes/helm/config.ts b/garden-service/src/plugins/kubernetes/helm/config.ts index 4e46582dd6..958cf31a1c 100644 --- a/garden-service/src/plugins/kubernetes/helm/config.ts +++ b/garden-service/src/plugins/kubernetes/helm/config.ts @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import Joi = require("joi") import { find } from "lodash" import { ServiceSpec } from "../../../config/service" @@ -17,6 +16,7 @@ import { joiEnvVars, joiUserIdentifier, DeepPrimitiveMap, + joi, } from "../../../config/common" import { Module, FileCopySpec } from "../../../types/module" import { containsSource, getReleaseName } from "./common" @@ -59,15 +59,15 @@ export interface HelmTestSpec extends BaseTestSpec { export interface HelmModule extends Module { } export type HelmModuleConfig = HelmModule["_ConfigType"] -const resourceSchema = Joi.object() +const resourceSchema = joi.object() .keys({ // TODO: consider allowing a `resource` field, that includes the kind and name (e.g. Deployment/my-deployment). // TODO: allow using a Pod directly - kind: Joi.string() + kind: joi.string() .only(...hotReloadableKinds) .default("Deployment") .description("The type of Kubernetes resource to sync files to."), - name: Joi.string() + name: joi.string() .description( deline`The name of the resource to sync to. If the chart contains a single resource of the specified Kind, this can be omitted. @@ -77,7 +77,7 @@ const resourceSchema = Joi.object() directly from the template in question in order to match it. Note that you may need to add single quotes around the string for the YAML to be parsed correctly.`, ), - containerName: Joi.string() + containerName: joi.string() .description( deline`The name of a container in the target. Specify this if the target contains more than one container and the main container is not the first container in the spec.`, @@ -93,7 +93,7 @@ const resourceSchema = Joi.object() Note: If you specify a module here, you don't need to specify it additionally under \`build.dependencies\``, ) .example("my-container-module"), - hotReloadArgs: Joi.array().items(Joi.string()) + hotReloadArgs: joi.array().items(joi.string()) .description( "If specified, overrides the arguments for the main container when running in hot-reload mode.", ) @@ -108,7 +108,7 @@ export const execTaskSchema = baseTestSpecSchema If not specified, the \`serviceResource\` configured on the module will be used. If neither is specified, an error will be thrown.`, ), - args: Joi.array().items(Joi.string()) + args: joi.array().items(joi.string()) .description("The arguments to pass to the pod used for execution."), env: joiEnvVars(), }) @@ -121,7 +121,7 @@ export const execTestSchema = baseTestSpecSchema If not specified, the \`serviceResource\` configured on the module will be used. If neither is specified, an error will be thrown.`, ), - args: Joi.array().items(Joi.string()) + args: joi.array().items(joi.string()) .description("The arguments to pass to the pod used for testing."), env: joiEnvVars(), }) @@ -143,13 +143,13 @@ export interface HelmServiceSpec extends ServiceSpec { export type HelmService = Service -const parameterValueSchema = Joi.alternatives( +const parameterValueSchema = joi.alternatives( joiPrimitive(), - Joi.array().items(Joi.lazy(() => parameterValueSchema)), - Joi.object().pattern(/.+/, Joi.lazy(() => parameterValueSchema)), + joi.array().items(joi.lazy(() => parameterValueSchema)), + joi.object().pattern(/.+/, joi.lazy(() => parameterValueSchema)), ) -export const helmModuleSpecSchema = Joi.object().keys({ +export const helmModuleSpecSchema = joi.object().keys({ base: joiUserIdentifier() .description( deline`The name of another \`helm\` module to use as a base for this one. Use this to re-use a Helm chart across @@ -162,14 +162,14 @@ export const helmModuleSpecSchema = Joi.object().keys({ ) .example("my-base-chart"), build: baseBuildSpecSchema, - chart: Joi.string() + chart: joi.string() .description( deline`A valid Helm chart name or URI (same as you'd input to \`helm install\`). Required if the module doesn't contain the Helm chart itself.`, ) .example("stable/nginx-ingress"), - chartPath: Joi.string() - .uri({ relativeOnly: true }) + chartPath: joi.string() + .posixPath({ subPathOnly: true }) .description( deline`The path, relative to the module path, to the chart sources (i.e. where the Chart.yaml file is, if any). Not used when \`base\` is specified.`, @@ -179,7 +179,7 @@ export const helmModuleSpecSchema = Joi.object().keys({ .description("List of names of services that should be deployed before this chart."), releaseName: joiIdentifier() .description("Optionally override the release name used when installing (defaults to the module name)."), - repo: Joi.string() + repo: joi.string() .description("The repository URL to fetch the chart from."), serviceResource: resourceSchema .description( @@ -191,7 +191,7 @@ export const helmModuleSpecSchema = Joi.object().keys({ We currently map a Helm chart to a single Garden service, because all the resources in a Helm chart are deployed at once.`, ), - skipDeploy: Joi.boolean() + skipDeploy: joi.boolean() .default(false) .description( deline`Set this to true if the chart should only be built, but not deployed as a service. @@ -201,9 +201,9 @@ export const helmModuleSpecSchema = Joi.object().keys({ .description("The task definitions for this module."), tests: joiArray(execTestSchema) .description("The test suite definitions for this module."), - version: Joi.string() + version: joi.string() .description("The chart version to deploy."), - values: Joi.object() + values: joi.object() .pattern(/.+/, parameterValueSchema) .default(() => ({}), "{}") .description( diff --git a/garden-service/src/plugins/kubernetes/helm/handlers.ts b/garden-service/src/plugins/kubernetes/helm/handlers.ts index adb81fe077..b01fcf0917 100644 --- a/garden-service/src/plugins/kubernetes/helm/handlers.ts +++ b/garden-service/src/plugins/kubernetes/helm/handlers.ts @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { ModuleAndRuntimeActions } from "../../../types/plugin/plugin" import { HelmModule, validateHelmModule as configureHelmModule, helmModuleSpecSchema } from "./config" import { buildHelmModule } from "./build" @@ -18,10 +17,11 @@ import { hotReloadHelmChart } from "./hot-reload" import { getServiceLogs } from "./logs" import { testHelmModule } from "./test" import { dedent } from "../../../util/string" +import { joi } from "../../../config/common" -const helmModuleOutputsSchema = Joi.object() +const helmModuleOutputsSchema = joi.object() .keys({ - "release-name": Joi.string() + "release-name": joi.string() .required() .description("The Helm release name of the service."), }) diff --git a/garden-service/src/plugins/kubernetes/kubernetes-module/config.ts b/garden-service/src/plugins/kubernetes/kubernetes-module/config.ts index e55568e4be..13478d7d1e 100644 --- a/garden-service/src/plugins/kubernetes/kubernetes-module/config.ts +++ b/garden-service/src/plugins/kubernetes/kubernetes-module/config.ts @@ -6,10 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import Joi = require("joi") - import { ServiceSpec } from "../../../config/service" -import { joiArray, joiIdentifier } from "../../../config/common" +import { joiArray, joiIdentifier, joi } from "../../../config/common" import { Module } from "../../../types/module" import { ConfigureModuleParams, ConfigureModuleResult } from "../../../types/plugin/module/configure" import { Service } from "../../../types/service" @@ -32,18 +30,18 @@ export interface KubernetesServiceSpec extends ServiceSpec { export type KubernetesService = Service -const kubernetesResourceSchema = Joi.object() +const kubernetesResourceSchema = joi.object() .keys({ - apiVersion: Joi.string() + apiVersion: joi.string() .required() .description("The API version of the resource."), - kind: Joi.string() + kind: joi.string() .required() .description("The kind of the resource."), - metadata: Joi.object() + metadata: joi.object() .required() .keys({ - name: Joi.string() + name: joi.string() .required() .description("The name of the resource."), }) @@ -51,7 +49,7 @@ const kubernetesResourceSchema = Joi.object() }) .unknown(true) -const kubernetesModuleSpecSchema = Joi.object() +const kubernetesModuleSpecSchema = joi.object() .keys({ build: baseBuildSpecSchema, dependencies: joiArray(joiIdentifier()) @@ -61,7 +59,7 @@ const kubernetesModuleSpecSchema = Joi.object() deline` List of Kubernetes resource manifests to deploy. Use this instead of the \`files\` field if you need to resolve template strings in any of the manifests.`), - files: joiArray(Joi.string().uri({ relativeOnly: true })) + files: joiArray(joi.string().posixPath({ subPathOnly: true })) .description("POSIX-style paths to YAML files to load manifests from. Each can contain multiple manifests."), }) @@ -79,7 +77,7 @@ export async function describeType() { If you need more advanced templating features you can use the [helm](https://docs.garden.io/reference/module-types/helm) module type. `, - outputsSchema: Joi.object().keys({}), + outputsSchema: joi.object().keys({}), schema: kubernetesModuleSpecSchema, } } diff --git a/garden-service/src/plugins/kubernetes/local/config.ts b/garden-service/src/plugins/kubernetes/local/config.ts index 521929e4e2..c1c1fd8744 100644 --- a/garden-service/src/plugins/kubernetes/local/config.ts +++ b/garden-service/src/plugins/kubernetes/local/config.ts @@ -7,10 +7,9 @@ */ import * as execa from "execa" -import * as Joi from "joi" import { KubernetesBaseConfig, kubernetesConfigBase, k8sContextSchema } from "../config" import { ConfigureProviderParams } from "../../../types/plugin/provider/configureProvider" -import { joiProviderName } from "../../../config/common" +import { joiProviderName, joi } from "../../../config/common" import { getKubeConfig } from "../api" import { configureMicrok8sAddons } from "./microk8s" import { setMinikubeDockerEnv } from "./minikube" @@ -31,13 +30,13 @@ export const configSchema = kubernetesConfigBase name: joiProviderName("local-kubernetes"), context: k8sContextSchema .optional(), - namespace: Joi.string() + namespace: joi.string() .default(undefined, "") .description( "Specify which namespace to deploy services to (defaults to the project name). " + "Note that the framework generates other namespaces as well with this name as a prefix.", ), - setupIngressController: Joi.string() + setupIngressController: joi.string() .allow("nginx", false, null) .default("nginx") .description("Set this to null or false to skip installing/enabling the `nginx` ingress controller."), diff --git a/garden-service/src/plugins/maven-container/maven-container.ts b/garden-service/src/plugins/maven-container/maven-container.ts index 1b81dd4d1b..e9a624fe0c 100644 --- a/garden-service/src/plugins/maven-container/maven-container.ts +++ b/garden-service/src/plugins/maven-container/maven-container.ts @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { omit, get } from "lodash" import { copy, pathExists, readFile } from "fs-extra" import { GardenPlugin } from "../../types/plugin/plugin" @@ -17,7 +16,7 @@ import { ContainerModuleConfig, ContainerTaskSpec, } from "../container/config" -import { joiArray, joiProviderName } from "../../config/common" +import { joiArray, joiProviderName, joi } from "../../config/common" import { Module } from "../../types/module" import { configureContainerModule, @@ -61,16 +60,17 @@ export interface MavenContainerModule< > extends Module { } const mavenKeys = { - jarPath: Joi.string() + jarPath: joi.string() .required() - .description("The path to the packaged JAR artifact, relative to the module directory.") + .posixPath({ subPathOnly: true }) + .description("POSIX-style path to the packaged JAR artifact, relative to the module directory.") .example("target/my-module.jar"), - jdkVersion: Joi.number() + jdkVersion: joi.number() .integer() .allow(8, 11) .default(8) .description("The JDK version to use."), - mvnOpts: joiArray(Joi.string()) + mvnOpts: joiArray(joi.string()) .description("Options to add to the `mvn package` command when building."), } diff --git a/garden-service/src/plugins/openfaas/openfaas.ts b/garden-service/src/plugins/openfaas/openfaas.ts index 2b31e18e0c..4424dc6944 100644 --- a/garden-service/src/plugins/openfaas/openfaas.ts +++ b/garden-service/src/plugins/openfaas/openfaas.ts @@ -7,12 +7,11 @@ */ import dedent = require("dedent") -import * as Joi from "joi" import { join } from "path" import { resolve as urlResolve } from "url" import { ConfigurationError } from "../../exceptions" import { PluginContext } from "../../plugin-context" -import { joiArray, PrimitiveMap, joiProviderName } from "../../config/common" +import { joiArray, PrimitiveMap, joiProviderName, joi } from "../../config/common" import { Module } from "../../types/module" import { ConfigureProviderResult } from "../../types/plugin/outputs" import { ServiceStatus, ServiceIngress, Service } from "../../types/service" @@ -57,24 +56,24 @@ export interface OpenFaasModuleSpec extends ExecModuleSpec { export const openfaasModuleSpecSchema = execModuleSpecSchema .keys({ - dependencies: joiArray(Joi.string()) + dependencies: joiArray(joi.string()) .description("The names of services/functions that this function depends on at runtime."), - handler: Joi.string() + handler: joi.string() .default(".") - .uri(({ relativeOnly: true })) + .posixPath({ subPathOnly: true }) .description("Specify which directory under the module contains the handler file/function."), - image: Joi.string() + image: joi.string() .description("The image name to use for the built OpenFaaS container (defaults to the module name)"), - lang: Joi.string() + lang: joi.string() .required() .description("The OpenFaaS language template to use to build this function."), }) .unknown(false) .description("The module specification for an OpenFaaS module.") -export const openfaasModuleOutputsSchema = Joi.object() +export const openfaasModuleOutputsSchema = joi.object() .keys({ - endpoint: Joi.string() + endpoint: joi.string() .uri() .required() .description(`The full URL to query this service _from within_ the cluster.`), @@ -91,7 +90,7 @@ export interface OpenFaasConfig extends ProviderConfig { export const configSchema = providerConfigBaseSchema .keys({ name: joiProviderName("openfaas"), - hostname: Joi.string() + hostname: joi.string() .hostname() .description(dedent` The hostname to configure for the function gateway. diff --git a/garden-service/src/server/commands.ts b/garden-service/src/server/commands.ts index 70e4e20d48..8375e9a296 100644 --- a/garden-service/src/server/commands.ts +++ b/garden-service/src/server/commands.ts @@ -6,10 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import Joi = require("joi") +import Joi = require("@hapi/joi") import Koa = require("koa") import { Command, Parameters, ParameterValues } from "../commands/base" -import { validate } from "../config/common" +import { validate, joi } from "../config/common" import { extend, mapValues, omitBy } from "lodash" import { Garden } from "../garden" import { LogLevel } from "../logger/log-node" @@ -24,13 +24,13 @@ export interface CommandMap { } } -const baseRequestSchema = Joi.object() +const baseRequestSchema = joi.object() .keys({ - command: Joi.string() + command: joi.string() .required() .description("The command name to run.") .example("get.status"), - parameters: Joi.object() + parameters: joi.object() .keys({}) .unknown(true) .default(() => ({}), "{}") @@ -94,7 +94,7 @@ export async function prepareCommands(): Promise { function addCommand(command: Command) { const requestSchema = baseRequestSchema .keys({ - parameters: Joi.object() + parameters: joi.object() .keys({ ...paramsToJoi(command.arguments), ...paramsToJoi({ ...GLOBAL_OPTIONS, ...command.options }), diff --git a/garden-service/src/server/server.ts b/garden-service/src/server/server.ts index 57f589bd05..917d9b30f3 100644 --- a/garden-service/src/server/server.ts +++ b/garden-service/src/server/server.ts @@ -9,7 +9,6 @@ import { Server } from "http" import chalk from "chalk" -import * as Joi from "joi" import Koa = require("koa") import mount = require("koa-mount") import serve = require("koa-static") @@ -28,6 +27,7 @@ import { toGardenError, GardenError } from "../exceptions" import { EventName, Events } from "../events" import { ValueOf } from "../util/util" import { AnalyticsHandler } from "../analytics/analytics" +import { joi } from "../config/common" export const DEFAULT_PORT = 9777 const notReadyMessage = "Waiting for Garden instance to initialize" @@ -195,13 +195,13 @@ export class GardenServer { const requestId = request.id try { - Joi.attempt(requestId, Joi.string().uuid().required()) + joi.attempt(requestId, joi.string().uuid().required()) } catch { return send("error", { message: "Message should contain an `id` field with a UUID value" }) } try { - Joi.attempt(request.type, Joi.string().required()) + joi.attempt(request.type, joi.string().required()) } catch { return send("error", { message: "Message should contain a type field" }) } diff --git a/garden-service/src/types/module.ts b/garden-service/src/types/module.ts index a3d4d38f73..5bb0e01eb5 100644 --- a/garden-service/src/types/module.ts +++ b/garden-service/src/types/module.ts @@ -15,8 +15,7 @@ import { TaskSpec } from "../config/task" import { ModuleVersion, moduleVersionSchema } from "../vcs/vcs" import { pathToCacheContext } from "../cache" import { Garden } from "../garden" -import * as Joi from "joi" -import { joiArray, joiIdentifier, joiIdentifierMap } from "../config/common" +import { joiArray, joiIdentifier, joiIdentifierMap, joi } from "../config/common" import { ConfigGraph } from "../config-graph" import * as Bluebird from "bluebird" import { getConfigFilePath } from "../util/fs" @@ -55,21 +54,18 @@ export interface Module< export const moduleSchema = moduleConfigSchema .keys({ - buildPath: Joi.string() + buildPath: joi.string() .required() - .uri({ relativeOnly: true }) .description("The path to the build staging directory for the module."), - buildMetadataPath: Joi.string() + buildMetadataPath: joi.string() .required() - .uri({ relativeOnly: true }) .description("The path to the build metadata directory for the module."), - configPath: Joi.string() + configPath: joi.string() .required() - .uri({ relativeOnly: true }) .description("The path to the module config file."), version: moduleVersionSchema .required(), - buildDependencies: joiIdentifierMap(Joi.lazy(() => moduleSchema)) + buildDependencies: joiIdentifierMap(joi.lazy(() => moduleSchema)) .required() .description("A map of all modules referenced under \`build.dependencies\`."), serviceNames: joiArray(joiIdentifier()) diff --git a/garden-service/src/types/plugin/base.ts b/garden-service/src/types/plugin/base.ts index c6ed56d1c8..0d3c13bfae 100644 --- a/garden-service/src/types/plugin/base.ts +++ b/garden-service/src/types/plugin/base.ts @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { LogEntry } from "../../logger/log-entry" import { PluginContext, pluginContextSchema } from "../../plugin-context" import { Module, moduleSchema } from "../module" @@ -14,6 +13,7 @@ import { RuntimeContext, Service, serviceSchema, runtimeContextSchema } from ".. import { Task } from "../task" import { taskSchema } from "../../config/task" import { ModuleVersion, moduleVersionSchema } from "../../vcs/vcs" +import { joi } from "../../config/common" export interface PluginActionContextParams { ctx: PluginContext @@ -24,11 +24,11 @@ export interface PluginActionParamsBase extends PluginActionContextParams { } // Note: not specifying this further because we will later remove it from the API -export const logEntrySchema = Joi.object() +export const logEntrySchema = joi.object() .description("Logging context handler that the handler can use to log messages and progress.") .required() -export const actionParamsSchema = Joi.object() +export const actionParamsSchema = joi.object() .keys({ ctx: pluginContextSchema .required(), @@ -64,12 +64,12 @@ export const taskActionParamsSchema = moduleActionParamsSchema }) export const runBaseParams = { - interactive: Joi.boolean() + interactive: joi.boolean() .description("Whether to run the module interactively (i.e. attach to the terminal)."), runtimeContext: runtimeContextSchema, - silent: Joi.boolean() + silent: joi.boolean() .description("Set to false if the output should not be logged to the console."), - timeout: Joi.number() + timeout: joi.number() .optional() .description("If set, how long to run the command before timing out."), } @@ -84,24 +84,24 @@ export interface RunResult { output: string } -export const runResultSchema = Joi.object() +export const runResultSchema = joi.object() .keys({ - moduleName: Joi.string() + moduleName: joi.string() .description("The name of the module that was run."), - command: Joi.array().items(Joi.string()) + command: joi.array().items(joi.string()) .required() .description("The command that was run in the module."), version: moduleVersionSchema, - success: Joi.boolean() + success: joi.boolean() .required() .description("Whether the module was successfully run."), - startedAt: Joi.date() + startedAt: joi.date() .required() .description("When the module run was started."), - completedAt: Joi.date() + completedAt: joi.date() .required() .description("When the module run was completed."), - output: Joi.string() + output: joi.string() .required() .allow("") .description("The output log from the run."), diff --git a/garden-service/src/types/plugin/module/build.ts b/garden-service/src/types/plugin/module/build.ts index 5a43f0f02f..a06c739365 100644 --- a/garden-service/src/types/plugin/module/build.ts +++ b/garden-service/src/types/plugin/module/build.ts @@ -6,10 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { dedent } from "../../../util/string" import { Module } from "../../module" import { PluginModuleActionParamsBase, moduleActionParamsSchema } from "../base" +import { joi } from "../../../config/common" export interface BuildModuleParams extends PluginModuleActionParamsBase { } @@ -30,20 +30,20 @@ export const build = { paramsSchema: moduleActionParamsSchema, - resultSchema: Joi.object() + resultSchema: joi.object() .keys({ - buildLog: Joi.string() + buildLog: joi.string() .allow("") .description("The full log from the build."), - fetched: Joi.boolean() + fetched: joi.boolean() .description("Set to true if the build was fetched from a remote registry."), - fresh: Joi.boolean() + fresh: joi.boolean() .description( "Set to true if the build was performed, false if it was already built, or fetched from a registry", ), - version: Joi.string() + version: joi.string() .description("The version that was built."), - details: Joi.object() + details: joi.object() .description("Additional information, specific to the provider."), }), } diff --git a/garden-service/src/types/plugin/module/configure.ts b/garden-service/src/types/plugin/module/configure.ts index 5049ce1b13..f3149e7454 100644 --- a/garden-service/src/types/plugin/module/configure.ts +++ b/garden-service/src/types/plugin/module/configure.ts @@ -6,13 +6,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { dedent } from "../../../util/string" import { Module } from "../../module" import { PluginContext, pluginContextSchema } from "../../../plugin-context" import { LogEntry } from "../../../logger/log-entry" import { logEntrySchema } from "../base" import { baseModuleSpecSchema, ModuleConfig, moduleConfigSchema } from "../../../config/module" +import { joi } from "../../../config/common" export interface ConfigureModuleParams { ctx: PluginContext @@ -43,7 +43,7 @@ export const configure = { any network calls. `, - paramsSchema: Joi.object() + paramsSchema: joi.object() .keys({ ctx: pluginContextSchema .required(), diff --git a/garden-service/src/types/plugin/module/describeType.ts b/garden-service/src/types/plugin/module/describeType.ts index 6e1e254459..a1ff0bae7f 100644 --- a/garden-service/src/types/plugin/module/describeType.ts +++ b/garden-service/src/types/plugin/module/describeType.ts @@ -6,11 +6,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" +import Joi = require("@hapi/joi") import { dedent } from "../../../util/string" +import { joi } from "../../../config/common" export interface DescribeModuleTypeParams { } -export const describeModuleTypeParamsSchema = Joi.object() +export const describeModuleTypeParamsSchema = joi.object() .keys({}) export interface ModuleTypeDescription { @@ -37,27 +38,27 @@ export const describeType = { any network calls. `, - paramsSchema: Joi.object().keys({}), + paramsSchema: joi.object().keys({}), - resultSchema: Joi.object() + resultSchema: joi.object() .keys({ - docs: Joi.string() + docs: joi.string() .required() .description("Documentation for the module type, in markdown format."), // TODO: specify the schemas using primitives and not Joi objects - outputsSchema: Joi.object() - .default(Joi.object().keys({}), "{}") + outputsSchema: joi.object() + .default(joi.object().keys({}), "{}") .description( "A valid Joi schema describing the keys that each module outputs, for use in template strings " + "(e.g. \`\${modules.my-module.outputs.some-key}\`).", ), - schema: Joi.object() + schema: joi.object() .required() .description( "A valid Joi schema describing the configuration keys for the `module` " + "field in the module's `garden.yml`.", ), - title: Joi.string() + title: joi.string() .description( "Readable title for the module type. Defaults to the title-cased type name, with dashes replaced by spaces.", ), diff --git a/garden-service/src/types/plugin/module/getBuildStatus.ts b/garden-service/src/types/plugin/module/getBuildStatus.ts index 7e98d26ec3..68e66d0f42 100644 --- a/garden-service/src/types/plugin/module/getBuildStatus.ts +++ b/garden-service/src/types/plugin/module/getBuildStatus.ts @@ -6,10 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { dedent } from "../../../util/string" import { Module } from "../../module" import { PluginModuleActionParamsBase, moduleActionParamsSchema } from "../base" +import { joi } from "../../../config/common" export interface GetBuildStatusParams extends PluginModuleActionParamsBase { } @@ -24,9 +24,9 @@ export const getBuildStatus = { Called before running the \`build\` action, which is not run if this returns \`{ ready: true }\`. `, paramsSchema: moduleActionParamsSchema, - resultSchema: Joi.object() + resultSchema: joi.object() .keys({ - ready: Joi.boolean() + ready: joi.boolean() .required() .description("Whether an up-to-date build is ready for the module."), }), diff --git a/garden-service/src/types/plugin/module/getTestResult.ts b/garden-service/src/types/plugin/module/getTestResult.ts index 0ee411e8cd..e2123ae118 100644 --- a/garden-service/src/types/plugin/module/getTestResult.ts +++ b/garden-service/src/types/plugin/module/getTestResult.ts @@ -6,11 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { dedent, deline } from "../../../util/string" import { Module } from "../../module" import { PluginModuleActionParamsBase, moduleActionParamsSchema, RunResult, runResultSchema } from "../base" import { ModuleVersion, moduleVersionSchema } from "../../../vcs/vcs" +import { joi } from "../../../config/common" export interface GetTestResultParams extends PluginModuleActionParamsBase { testName: string @@ -23,7 +23,7 @@ export interface TestResult extends RunResult { export const testResultSchema = runResultSchema .keys({ - testName: Joi.string() + testName: joi.string() .required() .description("The name of the test that was run."), }) @@ -45,7 +45,7 @@ export const getTestResult = { paramsSchema: moduleActionParamsSchema .keys({ - testName: Joi.string() + testName: joi.string() .description("A unique name to identify the test run."), testVersion: testVersionSchema, }), diff --git a/garden-service/src/types/plugin/module/publishModule.ts b/garden-service/src/types/plugin/module/publishModule.ts index c7e7f6da36..3d07b6ed84 100644 --- a/garden-service/src/types/plugin/module/publishModule.ts +++ b/garden-service/src/types/plugin/module/publishModule.ts @@ -6,10 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { dedent } from "../../../util/string" import { Module } from "../../module" import { PluginModuleActionParamsBase, moduleActionParamsSchema } from "../base" +import { joi } from "../../../config/common" export interface PublishModuleParams extends PluginModuleActionParamsBase { } @@ -25,12 +25,12 @@ export const publishModule = { Called by the \`garden publish\` command. `, paramsSchema: moduleActionParamsSchema, - resultSchema: Joi.object() + resultSchema: joi.object() .keys({ - published: Joi.boolean() + published: joi.boolean() .required() .description("Set to true if the module was published."), - message: Joi.string() + message: joi.string() .description("Optional result message."), }), } diff --git a/garden-service/src/types/plugin/module/runModule.ts b/garden-service/src/types/plugin/module/runModule.ts index 1014891b6f..d7c8543566 100644 --- a/garden-service/src/types/plugin/module/runModule.ts +++ b/garden-service/src/types/plugin/module/runModule.ts @@ -6,12 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { dedent } from "../../../util/string" import { Module } from "../../module" import { PluginModuleActionParamsBase, moduleActionParamsSchema, runBaseParams, runResultSchema } from "../base" import { RuntimeContext } from "../../service" -import { joiArray } from "../../../config/common" +import { joiArray, joi } from "../../../config/common" export interface RunModuleParams extends PluginModuleActionParamsBase { command?: string[] @@ -27,10 +26,10 @@ export const runModuleBaseSchema = moduleActionParamsSchema export const runModuleParamsSchema = runModuleBaseSchema .keys({ - command: joiArray(Joi.string()) + command: joiArray(joi.string()) .optional() .description("The command/entrypoint to run in the module."), - args: joiArray(Joi.string()) + args: joiArray(joi.string()) .description("The arguments passed to the command/entrypoint to run in the module."), }) diff --git a/garden-service/src/types/plugin/outputs.ts b/garden-service/src/types/plugin/outputs.ts index cb972a1ce1..eaf1e677ca 100644 --- a/garden-service/src/types/plugin/outputs.ts +++ b/garden-service/src/types/plugin/outputs.ts @@ -6,21 +6,21 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" +import Joi = require("@hapi/joi") import { ModuleVersion, moduleVersionSchema } from "../../vcs/vcs" import { Module } from "../module" import { ServiceStatus } from "../service" import { moduleConfigSchema, ModuleConfig } from "../../config/module" import { DashboardPage, dashboardPagesSchema } from "../../config/dashboard" import { ProviderConfig, providerConfigBaseSchema } from "../../config/provider" -import { joiArray } from "../../config/common" +import { joiArray, joi } from "../../config/common" import { deline } from "../../util/string" export interface ConfigureProviderResult { config: T moduleConfigs?: ModuleConfig[] } -export const configureProviderResultSchema = Joi.object() +export const configureProviderResultSchema = joi.object() .keys({ config: providerConfigBaseSchema, moduleConfigs: joiArray(moduleConfigSchema) @@ -40,18 +40,18 @@ export interface EnvironmentStatus { detail?: any } -export const environmentStatusSchema = Joi.object() +export const environmentStatusSchema = joi.object() .keys({ - ready: Joi.boolean() + ready: joi.boolean() .required() .description("Set to true if the environment is fully configured for a provider."), - needUserInput: Joi.boolean() + needUserInput: joi.boolean() .description( "Set to true if the environment needs user input to be initialized, " + "and thus needs to be initialized via `garden init`.", ), dashboardPages: dashboardPagesSchema, - detail: Joi.object() + detail: joi.object() .meta({ extendable: true }) .description("Use this to include additional information that is specific to the provider."), }) @@ -63,19 +63,19 @@ export type EnvironmentStatusMap = { export interface PrepareEnvironmentResult { } -export const prepareEnvironmentResultSchema = Joi.object().keys({}) +export const prepareEnvironmentResultSchema = joi.object().keys({}) export interface CleanupEnvironmentResult { } -export const cleanupEnvironmentResultSchema = Joi.object().keys({}) +export const cleanupEnvironmentResultSchema = joi.object().keys({}) export interface GetSecretResult { value: string | null } -export const getSecretResultSchema = Joi.object() +export const getSecretResultSchema = joi.object() .keys({ - value: Joi.string() + value: joi.string() .allow(null) .required() .description("The config value found for the specified key (as string), or null if not found."), @@ -83,15 +83,15 @@ export const getSecretResultSchema = Joi.object() export interface SetSecretResult { } -export const setSecretResultSchema = Joi.object().keys({}) +export const setSecretResultSchema = joi.object().keys({}) export interface DeleteSecretResult { found: boolean } -export const deleteSecretResultSchema = Joi.object() +export const deleteSecretResultSchema = joi.object() .keys({ - found: Joi.boolean() + found: joi.boolean() .required() .description("Set to true if the key was deleted, false if it was not found."), }) @@ -103,19 +103,19 @@ export interface ExecInServiceResult { stderr?: string } -export const execInServiceResultSchema = Joi.object() +export const execInServiceResultSchema = joi.object() .keys({ - code: Joi.number() + code: joi.number() .required() .description("The exit code of the command executed in the service container."), - output: Joi.string() + output: joi.string() .allow("") .required() .description("The output of the executed command."), - stdout: Joi.string() + stdout: joi.string() .allow("") .description("The stdout output of the executed command (if available)."), - stderr: Joi.string() + stderr: joi.string() .allow("") .description("The stderr output of the executed command (if available)."), }) @@ -126,15 +126,15 @@ export interface ServiceLogEntry { msg: string } -export const serviceLogEntrySchema = Joi.object() +export const serviceLogEntrySchema = joi.object() .keys({ - serviceName: Joi.string() + serviceName: joi.string() .required() .description("The name of the service the log entry originated from."), - timestamp: Joi.date() + timestamp: joi.date() .required() .description("The time when the log entry was generated by the service."), - msg: Joi.string() + msg: joi.string() .required() .description("The content of the log entry."), }) @@ -142,7 +142,7 @@ export const serviceLogEntrySchema = Joi.object() export interface GetServiceLogsResult { } -export const getServiceLogsResultSchema = Joi.object().keys({}) +export const getServiceLogsResultSchema = joi.object().keys({}) export interface ModuleTypeDescription { docs: string @@ -151,18 +151,18 @@ export interface ModuleTypeDescription { title?: string } -export const moduleTypeDescriptionSchema = Joi.object() +export const moduleTypeDescriptionSchema = joi.object() .keys({ - docs: Joi.string() + docs: joi.string() .required() .description("Documentation for the module type, in markdown format."), - schema: Joi.object() + schema: joi.object() .required() .description( "A valid Joi schema describing the configuration keys for the `module` " + "field in the module's `garden.yml`.", ), - title: Joi.string() + title: joi.string() .description( "Readable title for the module type. Defaults to the title-cased type name, with dashes replaced by spaces.", ), @@ -185,34 +185,34 @@ export interface BuildResult { version?: string details?: any } -export const buildModuleResultSchema = Joi.object() +export const buildModuleResultSchema = joi.object() .keys({ - buildLog: Joi.string() + buildLog: joi.string() .allow("") .description("The full log from the build."), - fetched: Joi.boolean() + fetched: joi.boolean() .description("Set to true if the build was fetched from a remote registry."), - fresh: Joi.boolean() + fresh: joi.boolean() .description("Set to true if the build was performed, false if it was already built, or fetched from a registry"), - version: Joi.string() + version: joi.string() .description("The version that was built."), - details: Joi.object() + details: joi.object() .description("Additional information, specific to the provider."), }) export interface HotReloadServiceResult { } -export const hotReloadServiceResultSchema = Joi.object() +export const hotReloadServiceResultSchema = joi.object() export interface PublishResult { published: boolean message?: string } -export const publishModuleResultSchema = Joi.object() +export const publishModuleResultSchema = joi.object() .keys({ - published: Joi.boolean() + published: joi.boolean() .required() .description("Set to true if the module was published."), - message: Joi.string() + message: joi.string() .description("Optional result message."), }) @@ -226,24 +226,24 @@ export interface RunResult { output: string } -export const runResultSchema = Joi.object() +export const runResultSchema = joi.object() .keys({ - moduleName: Joi.string() + moduleName: joi.string() .description("The name of the module that was run."), - command: Joi.array().items(Joi.string()) + command: joi.array().items(joi.string()) .required() .description("The command that was run in the module."), version: moduleVersionSchema, - success: Joi.boolean() + success: joi.boolean() .required() .description("Whether the module was successfully run."), - startedAt: Joi.date() + startedAt: joi.date() .required() .description("When the module run was started."), - completedAt: Joi.date() + completedAt: joi.date() .required() .description("When the module run was completed."), - output: Joi.string() + output: joi.string() .required() .allow("") .description("The output log from the run."), @@ -255,7 +255,7 @@ export interface TestResult extends RunResult { export const testResultSchema = runResultSchema .keys({ - testName: Joi.string() + testName: joi.string() .required() .description("The name of the test that was run."), }) @@ -266,9 +266,9 @@ export interface BuildStatus { ready: boolean } -export const buildStatusSchema = Joi.object() +export const buildStatusSchema = joi.object() .keys({ - ready: Joi.boolean() + ready: joi.boolean() .required() .description("Whether an up-to-date build is ready for the module."), }) @@ -284,26 +284,26 @@ export interface RunTaskResult extends RunResult { output: string } -export const runTaskResultSchema = Joi.object() +export const runTaskResultSchema = joi.object() .keys({ - moduleName: Joi.string() + moduleName: joi.string() .description("The name of the module that the task belongs to."), - taskName: Joi.string() + taskName: joi.string() .description("The name of the task that was run."), - command: Joi.array().items(Joi.string()) + command: joi.array().items(joi.string()) .required() .description("The command that the task ran in the module."), version: moduleVersionSchema, - success: Joi.boolean() + success: joi.boolean() .required() .description("Whether the task was successfully run."), - startedAt: Joi.date() + startedAt: joi.date() .required() .description("When the task run was started."), - completedAt: Joi.date() + completedAt: joi.date() .required() .description("When the task run was completed."), - output: Joi.string() + output: joi.string() .required() .allow("") .description("The output log from the run."), diff --git a/garden-service/src/types/plugin/params.ts b/garden-service/src/types/plugin/params.ts index 761eaa2cd0..41e3ea848e 100644 --- a/garden-service/src/types/plugin/params.ts +++ b/garden-service/src/types/plugin/params.ts @@ -6,12 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import Stream from "ts-stream" import { LogEntry } from "../../logger/log-entry" import { PluginContext, pluginContextSchema } from "../../plugin-context" import { ModuleVersion, moduleVersionSchema } from "../../vcs/vcs" -import { Primitive, joiPrimitive, joiArray } from "../../config/common" +import { Primitive, joiPrimitive, joiArray, joi } from "../../config/common" import { Module, moduleSchema } from "../module" import { RuntimeContext, Service, serviceSchema, runtimeContextSchema } from "../service" import { Task } from "../task" @@ -33,11 +32,11 @@ export interface PluginActionParamsBase extends PluginActionContextParams { } // Note: not specifying this further because we will later remove it from the API -const logEntrySchema = Joi.object() +const logEntrySchema = joi.object() .description("Logging context handler that the handler can use to log messages and progress.") .required() -const actionParamsSchema = Joi.object() +const actionParamsSchema = joi.object() .keys({ ctx: pluginContextSchema .required(), @@ -83,7 +82,7 @@ export interface ConfigureProviderParams { dependencies: Provider[] configStore: ConfigStore } -export const configureProviderParamsSchema = Joi.object() +export const configureProviderParamsSchema = joi.object() .keys({ config: providerConfigBaseSchema.required(), log: logEntrySchema, @@ -102,7 +101,7 @@ export interface PrepareEnvironmentParams extends PluginActionParamsBase { export const prepareEnvironmentParamsSchema = actionParamsSchema .keys({ status: environmentStatusSchema, - force: Joi.boolean() + force: joi.boolean() .description("Force re-configuration of the environment."), }) @@ -115,7 +114,7 @@ export interface GetSecretParams extends PluginActionParamsBase { } export const getSecretParamsSchema = actionParamsSchema .keys({ - key: Joi.string() + key: joi.string() .description("A unique identifier for the secret."), }) @@ -150,7 +149,7 @@ export interface PluginActionParams { * Module actions */ export interface DescribeModuleTypeParams { } -export const describeModuleTypeParamsSchema = Joi.object() +export const describeModuleTypeParamsSchema = joi.object() .keys({}) export interface ConfigureModuleParams { @@ -158,7 +157,7 @@ export interface ConfigureModuleParams { log: LogEntry moduleConfig: T["_ConfigType"] } -export const configureModuleParamsSchema = Joi.object() +export const configureModuleParamsSchema = joi.object() .keys({ ctx: pluginContextSchema .required(), @@ -184,12 +183,12 @@ export interface RunModuleParams extends PluginModule timeout?: number } const runBaseParams = { - interactive: Joi.boolean() + interactive: joi.boolean() .description("Whether to run the module interactively (i.e. attach to the terminal)."), runtimeContext: runtimeContextSchema, - silent: Joi.boolean() + silent: joi.boolean() .description("Set to false if the output should not be logged to the console."), - timeout: Joi.number() + timeout: joi.number() .optional() .description("If set, how long to run the command before timing out."), } @@ -197,7 +196,7 @@ const runModuleBaseSchema = moduleActionParamsSchema .keys(runBaseParams) export const runModuleParamsSchema = runModuleBaseSchema .keys({ - command: joiArray(Joi.string()) + command: joiArray(joi.string()) .description("The command to run in the module."), }) @@ -222,7 +221,7 @@ export interface GetTestResultParams extends PluginMo } export const getTestResultParamsSchema = moduleActionParamsSchema .keys({ - testName: Joi.string() + testName: joi.string() .description("A unique name to identify the test run."), testVersion: testVersionSchema, }) @@ -242,7 +241,7 @@ export interface GetServiceStatusParams @@ -304,14 +303,14 @@ export interface GetServiceLogsParams Joi.func())) + actions: joi.object().keys(mapValues(pluginActionDescriptions, () => joi.func())) .description("A map of plugin action handlers provided by the plugin."), moduleActions: joiIdentifierMap( - Joi.object().keys(mapValues(moduleActionDescriptions, () => Joi.func()), + joi.object().keys(mapValues(moduleActionDescriptions, () => joi.func()), ).description("A map of module names and module action handlers provided by the plugin."), ), }) .description("The schema for Garden plugins.") -export const pluginModuleSchema = Joi.object() +export const pluginModuleSchema = joi.object() .keys({ name: joiIdentifier(), - gardenPlugin: Joi.func().required() + gardenPlugin: joi.func().required() .description("The initialization function for the plugin. Should return a valid Garden plugin object."), }) .unknown(true) diff --git a/garden-service/src/types/plugin/provider/cleanupEnvironment.ts b/garden-service/src/types/plugin/provider/cleanupEnvironment.ts index 2e13c45ad5..74ebbf1f75 100644 --- a/garden-service/src/types/plugin/provider/cleanupEnvironment.ts +++ b/garden-service/src/types/plugin/provider/cleanupEnvironment.ts @@ -6,9 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { PluginActionParamsBase, actionParamsSchema } from "../base" import { dedent } from "../../../util/string" +import { joi } from "../../../config/common" export interface CleanupEnvironmentParams extends PluginActionParamsBase { } @@ -24,5 +24,5 @@ export const cleanupEnvironment = { Called by the \`garden delete environment\` command. `, paramsSchema: actionParamsSchema, - resultSchema: Joi.object().keys({}), + resultSchema: joi.object().keys({}), } diff --git a/garden-service/src/types/plugin/provider/configureProvider.ts b/garden-service/src/types/plugin/provider/configureProvider.ts index 711b55dce9..31d13e7600 100644 --- a/garden-service/src/types/plugin/provider/configureProvider.ts +++ b/garden-service/src/types/plugin/provider/configureProvider.ts @@ -6,14 +6,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import dedent = require("dedent") import { projectNameSchema } from "../../../config/project" import { ProviderConfig, Provider, providerConfigBaseSchema, providersSchema } from "../../../config/provider" import { LogEntry } from "../../../logger/log-entry" import { logEntrySchema } from "../base" import { configStoreSchema, ConfigStore } from "../../../config-store" -import { joiArray } from "../../../config/common" +import { joiArray, joi } from "../../../config/common" import { moduleConfigSchema, ModuleConfig } from "../../../config/module" import { deline } from "../../../util/string" @@ -42,7 +41,7 @@ export const configureProvider = { Important: This action is called on most executions of Garden commands, so it should return quickly and avoid performing expensive processing or network calls. `, - paramsSchema: Joi.object() + paramsSchema: joi.object() .keys({ config: providerConfigBaseSchema.required(), log: logEntrySchema, @@ -50,7 +49,7 @@ export const configureProvider = { dependencies: providersSchema, configStore: configStoreSchema, }), - resultSchema: Joi.object() + resultSchema: joi.object() .keys({ config: providerConfigBaseSchema, moduleConfigs: joiArray(moduleConfigSchema) diff --git a/garden-service/src/types/plugin/provider/deleteSecret.ts b/garden-service/src/types/plugin/provider/deleteSecret.ts index 6062c14729..5e0b87022e 100644 --- a/garden-service/src/types/plugin/provider/deleteSecret.ts +++ b/garden-service/src/types/plugin/provider/deleteSecret.ts @@ -6,10 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { PluginActionParamsBase } from "../base" import { dedent } from "../../../util/string" import { getSecretParamsSchema } from "./getSecret" +import { joi } from "../../../config/common" export interface DeleteSecretParams extends PluginActionParamsBase { key: string @@ -24,9 +24,9 @@ export const deleteSecret = { Remove a secret for this plugin in the current environment (as set via \`setSecret\`). `, paramsSchema: getSecretParamsSchema, - resultSchema: Joi.object() + resultSchema: joi.object() .keys({ - found: Joi.boolean() + found: joi.boolean() .required() .description("Set to true if the key was deleted, false if it was not found."), }), diff --git a/garden-service/src/types/plugin/provider/getDebugInfo.ts b/garden-service/src/types/plugin/provider/getDebugInfo.ts index 14a3216264..a4c1b94bc6 100644 --- a/garden-service/src/types/plugin/provider/getDebugInfo.ts +++ b/garden-service/src/types/plugin/provider/getDebugInfo.ts @@ -6,10 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" - import { actionParamsSchema, PluginActionParamsBase } from "../base" import { dedent } from "../../../util/string" +import { joi } from "../../../config/common" export interface DebugInfo { info: any @@ -26,9 +25,9 @@ export const getDebugInfo = { Collects debug info from the provider. `, paramsSchema: actionParamsSchema, - resultSchema: Joi.object() + resultSchema: joi.object() .keys({ - info: Joi.any() + info: joi.any() .required() .description("An object representing the debug info for the project."), }), diff --git a/garden-service/src/types/plugin/provider/getEnvironmentStatus.ts b/garden-service/src/types/plugin/provider/getEnvironmentStatus.ts index b1b7cc9f9d..d8a9c15a7b 100644 --- a/garden-service/src/types/plugin/provider/getEnvironmentStatus.ts +++ b/garden-service/src/types/plugin/provider/getEnvironmentStatus.ts @@ -6,11 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" - import { DashboardPage, dashboardPagesSchema } from "../../../config/dashboard" import { PluginActionParamsBase, actionParamsSchema } from "../base" import { dedent } from "../../../util/string" +import { joi } from "../../../config/common" export interface GetEnvironmentStatusParams extends PluginActionParamsBase { } @@ -25,18 +24,18 @@ export interface EnvironmentStatusMap { [providerName: string]: EnvironmentStatus } -export const environmentStatusSchema = Joi.object() +export const environmentStatusSchema = joi.object() .keys({ - ready: Joi.boolean() + ready: joi.boolean() .required() .description("Set to true if the environment is fully configured for a provider."), - needManualInit: Joi.boolean() + needManualInit: joi.boolean() .description( "Set to true if the environment needs user input to be initialized, " + "and thus needs to be initialized via `garden init`.", ), dashboardPages: dashboardPagesSchema, - detail: Joi.object() + detail: joi.object() .meta({ extendable: true }) .description("Use this to include additional information that is specific to the provider."), }) diff --git a/garden-service/src/types/plugin/provider/getSecret.ts b/garden-service/src/types/plugin/provider/getSecret.ts index 591442deee..b2082ee535 100644 --- a/garden-service/src/types/plugin/provider/getSecret.ts +++ b/garden-service/src/types/plugin/provider/getSecret.ts @@ -6,9 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { PluginActionParamsBase, actionParamsSchema } from "../base" import { dedent } from "../../../util/string" +import { joi } from "../../../config/common" export interface GetSecretParams extends PluginActionParamsBase { key: string @@ -20,7 +20,7 @@ export interface GetSecretResult { export const getSecretParamsSchema = actionParamsSchema .keys({ - key: Joi.string() + key: joi.string() .description("A unique identifier for the secret."), }) @@ -29,9 +29,9 @@ export const getSecret = { Retrieve a secret value for this plugin in the current environment (as set via \`setSecret\`). `, paramsSchema: getSecretParamsSchema, - resultSchema: Joi.object() + resultSchema: joi.object() .keys({ - value: Joi.string() + value: joi.string() .allow(null) .required() .description("The config value found for the specified key (as string), or null if not found."), diff --git a/garden-service/src/types/plugin/provider/prepareEnvironment.ts b/garden-service/src/types/plugin/provider/prepareEnvironment.ts index 7440818ab2..0a53e19fd9 100644 --- a/garden-service/src/types/plugin/provider/prepareEnvironment.ts +++ b/garden-service/src/types/plugin/provider/prepareEnvironment.ts @@ -6,10 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { PluginActionParamsBase, actionParamsSchema } from "../base" import { environmentStatusSchema, EnvironmentStatus } from "./getEnvironmentStatus" import { dedent } from "../../../util/string" +import { joi } from "../../../config/common" export interface PrepareEnvironmentParams extends PluginActionParamsBase { manualInit: boolean @@ -35,11 +35,11 @@ export const prepareEnvironment = { `, paramsSchema: actionParamsSchema .keys({ - force: Joi.boolean() + force: joi.boolean() .description("Force re-configuration of the environment."), - manualInit: Joi.boolean() + manualInit: joi.boolean() .description("Set to true if the environment is being explicitly initialized via `garden init`."), status: environmentStatusSchema, }), - resultSchema: Joi.object().keys({}), + resultSchema: joi.object().keys({}), } diff --git a/garden-service/src/types/plugin/provider/setSecret.ts b/garden-service/src/types/plugin/provider/setSecret.ts index 27e3c1e216..0e75a1f8b8 100644 --- a/garden-service/src/types/plugin/provider/setSecret.ts +++ b/garden-service/src/types/plugin/provider/setSecret.ts @@ -6,10 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { PluginActionParamsBase } from "../base" import { dedent } from "../../../util/string" -import { joiPrimitive, Primitive } from "../../../config/common" +import { joiPrimitive, Primitive, joi } from "../../../config/common" import { getSecretParamsSchema } from "./getSecret" export interface SetSecretParams extends PluginActionParamsBase { @@ -30,5 +29,5 @@ export const setSecret = { value: joiPrimitive() .description("The value of the secret."), }), - resultSchema: Joi.object().keys({}), + resultSchema: joi.object().keys({}), } diff --git a/garden-service/src/types/plugin/service/deployService.ts b/garden-service/src/types/plugin/service/deployService.ts index f29fdffb7f..302f7152f0 100644 --- a/garden-service/src/types/plugin/service/deployService.ts +++ b/garden-service/src/types/plugin/service/deployService.ts @@ -6,11 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { PluginServiceActionParamsBase, serviceActionParamsSchema } from "../base" import { dedent } from "../../../util/string" import { Module } from "../../module" import { RuntimeContext, runtimeContextSchema, serviceStatusSchema } from "../../service" +import { joi } from "../../../config/common" export interface DeployServiceParams extends PluginServiceActionParamsBase { @@ -28,10 +28,10 @@ export const deployService = { `, paramsSchema: serviceActionParamsSchema .keys({ - force: Joi.boolean() + force: joi.boolean() .description("Whether to force a re-deploy, even if the service is already deployed."), runtimeContext: runtimeContextSchema, - hotReload: Joi.boolean() + hotReload: joi.boolean() .default(false) .description("Whether to configure the service for hot-reloading."), }), diff --git a/garden-service/src/types/plugin/service/execInService.ts b/garden-service/src/types/plugin/service/execInService.ts index ff0920f145..f2f12c9767 100644 --- a/garden-service/src/types/plugin/service/execInService.ts +++ b/garden-service/src/types/plugin/service/execInService.ts @@ -6,12 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { PluginServiceActionParamsBase, serviceActionParamsSchema } from "../base" import { dedent } from "../../../util/string" import { Module } from "../../module" import { RuntimeContext, runtimeContextSchema } from "../../service" -import { joiArray } from "../../../config/common" +import { joiArray, joi } from "../../../config/common" export interface ExecInServiceParams extends PluginServiceActionParamsBase { @@ -36,25 +35,25 @@ export const execInService = { paramsSchema: serviceActionParamsSchema .keys({ - command: joiArray(Joi.string()) + command: joiArray(joi.string()) .description("The command to run alongside the service."), runtimeContext: runtimeContextSchema, - interactive: Joi.boolean(), + interactive: joi.boolean(), }), - resultSchema: Joi.object() + resultSchema: joi.object() .keys({ - code: Joi.number() + code: joi.number() .required() .description("The exit code of the command executed in the service container."), - output: Joi.string() + output: joi.string() .allow("") .required() .description("The output of the executed command."), - stdout: Joi.string() + stdout: joi.string() .allow("") .description("The stdout output of the executed command (if available)."), - stderr: Joi.string() + stderr: joi.string() .allow("") .description("The stderr output of the executed command (if available)."), }), diff --git a/garden-service/src/types/plugin/service/getServiceLogs.ts b/garden-service/src/types/plugin/service/getServiceLogs.ts index 839426b11d..0f13a3adfd 100644 --- a/garden-service/src/types/plugin/service/getServiceLogs.ts +++ b/garden-service/src/types/plugin/service/getServiceLogs.ts @@ -6,12 +6,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { Stream } from "ts-stream" import { PluginServiceActionParamsBase, serviceActionParamsSchema } from "../base" import { dedent } from "../../../util/string" import { Module } from "../../module" import { RuntimeContext, runtimeContextSchema } from "../../service" +import { joi } from "../../../config/common" export interface GetServiceLogsParams extends PluginServiceActionParamsBase { @@ -28,15 +28,15 @@ export interface ServiceLogEntry { msg: string } -export const serviceLogEntrySchema = Joi.object() +export const serviceLogEntrySchema = joi.object() .keys({ - serviceName: Joi.string() + serviceName: joi.string() .required() .description("The name of the service the log entry originated from."), - timestamp: Joi.date() + timestamp: joi.date() .required() .description("The time when the log entry was generated by the service."), - msg: Joi.string() + msg: joi.string() .required() .description("The content of the log entry."), }) @@ -54,17 +54,17 @@ export const getServiceLogs = { paramsSchema: serviceActionParamsSchema .keys({ runtimeContext: runtimeContextSchema, - stream: Joi.object() + stream: joi.object() .description("A Stream object, to write the logs to."), - follow: Joi.boolean() + follow: joi.boolean() .description("Whether to keep listening for logs until aborted."), - tail: Joi.number() + tail: joi.number() .description("Number of lines to get from end of log. Defaults to -1, showing all log lines.") .default(-1), - startTime: Joi.date() + startTime: joi.date() .optional() .description("If set, only return logs that are as new or newer than this date."), }), - resultSchema: Joi.object().keys({}), + resultSchema: joi.object().keys({}), } diff --git a/garden-service/src/types/plugin/service/getServiceStatus.ts b/garden-service/src/types/plugin/service/getServiceStatus.ts index a9575f9ff5..ac6a24cab4 100644 --- a/garden-service/src/types/plugin/service/getServiceStatus.ts +++ b/garden-service/src/types/plugin/service/getServiceStatus.ts @@ -6,11 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { PluginServiceActionParamsBase, serviceActionParamsSchema } from "../base" import { dedent } from "../../../util/string" import { Module } from "../../module" import { RuntimeContext, runtimeContextSchema, serviceStatusSchema } from "../../service" +import { joi } from "../../../config/common" export type hotReloadStatus = "enabled" | "disabled" @@ -30,7 +30,7 @@ export const getServiceStatus = { paramsSchema: serviceActionParamsSchema .keys({ runtimeContext: runtimeContextSchema, - hotReload: Joi.boolean() + hotReload: joi.boolean() .default(false) .description("Whether the service should be configured for hot-reloading."), }), diff --git a/garden-service/src/types/plugin/service/hotReloadService.ts b/garden-service/src/types/plugin/service/hotReloadService.ts index 9cc785147d..153b1a6189 100644 --- a/garden-service/src/types/plugin/service/hotReloadService.ts +++ b/garden-service/src/types/plugin/service/hotReloadService.ts @@ -6,11 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { PluginServiceActionParamsBase, serviceActionParamsSchema } from "../base" import { dedent } from "../../../util/string" import { Module } from "../../module" import { RuntimeContext, runtimeContextSchema } from "../../service" +import { joi } from "../../../config/common" export interface HotReloadServiceParams extends PluginServiceActionParamsBase { @@ -25,5 +25,5 @@ export const hotReloadService = { `, paramsSchema: serviceActionParamsSchema .keys({ runtimeContext: runtimeContextSchema }), - resultSchema: Joi.object().keys({}), + resultSchema: joi.object().keys({}), } diff --git a/garden-service/src/types/plugin/task/getTaskResult.ts b/garden-service/src/types/plugin/task/getTaskResult.ts index 5028962e20..21629d54ce 100644 --- a/garden-service/src/types/plugin/task/getTaskResult.ts +++ b/garden-service/src/types/plugin/task/getTaskResult.ts @@ -6,11 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { taskActionParamsSchema, PluginTaskActionParamsBase } from "../base" import { dedent, deline } from "../../../util/string" import { Module } from "../../module" import { moduleVersionSchema, ModuleVersion } from "../../../vcs/vcs" +import { joi } from "../../../config/common" export const taskVersionSchema = moduleVersionSchema .description(deline` @@ -21,26 +21,26 @@ export interface GetTaskResultParams extends PluginTa taskVersion: ModuleVersion } -export const taskResultSchema = Joi.object() +export const taskResultSchema = joi.object() .keys({ - moduleName: Joi.string() + moduleName: joi.string() .description("The name of the module that the task belongs to."), - taskName: Joi.string() + taskName: joi.string() .description("The name of the task that was run."), - command: Joi.array().items(Joi.string()) + command: joi.array().items(joi.string()) .required() .description("The command that the task ran in the module."), version: moduleVersionSchema, - success: Joi.boolean() + success: joi.boolean() .required() .description("Whether the task was successfully run."), - startedAt: Joi.date() + startedAt: joi.date() .required() .description("When the task run was started."), - completedAt: Joi.date() + completedAt: joi.date() .required() .description("When the task run was completed."), - output: Joi.string() + output: joi.string() .required() .allow("") .description("The output log from the run."), diff --git a/garden-service/src/types/service.ts b/garden-service/src/types/service.ts index 240946be19..7e56bcaa55 100644 --- a/garden-service/src/types/service.ts +++ b/garden-service/src/types/service.ts @@ -6,9 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Joi from "joi" import { getEnvVarName, uniqByName } from "../util/util" -import { PrimitiveMap, joiEnvVars, joiIdentifierMap, joiPrimitive, joiUserIdentifier } from "../config/common" +import { PrimitiveMap, joiEnvVars, joiIdentifierMap, joiPrimitive, joiUserIdentifier, joi } from "../config/common" import { Module, getModuleKey } from "./module" import { ServiceConfig, serviceConfigSchema } from "../config/service" import dedent = require("dedent") @@ -27,15 +26,15 @@ export interface Service { spec: M["serviceConfigs"][0]["spec"] } -export const serviceSchema = Joi.object() +export const serviceSchema = joi.object() .options({ presence: "required" }) .keys({ name: joiUserIdentifier() .description("The name of the service."), - module: Joi.object().unknown(true), // This causes a stack overflow: Joi.lazy(() => moduleSchema), - sourceModule: Joi.object().unknown(true), // This causes a stack overflow: Joi.lazy(() => moduleSchema), + module: joi.object().unknown(true), // This causes a stack overflow: joi.lazy(() => moduleSchema), + sourceModule: joi.object().unknown(true), // This causes a stack overflow: joi.lazy(() => moduleSchema), config: serviceConfigSchema, - spec: Joi.object() + spec: joi.object() .description("The raw configuration of the service (specific to each plugin)."), }) @@ -93,7 +92,7 @@ export interface ServiceIngress extends ServiceIngressSpec { hostname: string } -export const ingressHostnameSchema = Joi.string() +export const ingressHostnameSchema = joi.string() .hostname() .description(dedent` The hostname that should route to this service. Defaults to the default hostname @@ -102,20 +101,20 @@ export const ingressHostnameSchema = Joi.string() Note that if you're developing locally you may need to add this hostname to your hosts file. `) -const portSchema = Joi.number() +const portSchema = joi.number() .description(dedent` The port number that the service is exposed on internally. This defaults to the first specified port for the service. `) -export const serviceIngressSpecSchema = Joi.object() +export const serviceIngressSpecSchema = joi.object() .keys({ hostname: ingressHostnameSchema, port: portSchema, - path: Joi.string() + path: joi.string() .default("/") .description("The ingress path that should be matched to route to this service."), - protocol: Joi.string() + protocol: joi.string() .only("http", "https") .required() .description("The protocol to use for the ingress."), @@ -123,7 +122,7 @@ export const serviceIngressSpecSchema = Joi.object() export const serviceIngressSchema = serviceIngressSpecSchema .keys({ - hostname: Joi.string() + hostname: joi.string() .required() .description("The hostname where the service can be accessed."), port: portSchema @@ -151,33 +150,33 @@ export interface ServiceStatusMap { [key: string]: ServiceStatus } -export const serviceStatusSchema = Joi.object() +export const serviceStatusSchema = joi.object() .keys({ - providerId: Joi.string() + providerId: joi.string() .description("The ID used for the service by the provider (if not the same as the service name)."), - providerVersion: Joi.string() + providerVersion: joi.string() .description("The provider version of the deployed service (if different from the Garden module version."), - version: Joi.string() + version: joi.string() .description("The Garden module version of the deployed service."), - state: Joi.string() + state: joi.string() .only("ready", "deploying", "stopped", "unhealthy", "unknown", "outdated", "missing") .default("unknown") .description("The current deployment status of the service."), - runningReplicas: Joi.number() + runningReplicas: joi.number() .description("How many replicas of the service are currently running."), - ingresses: Joi.array() + ingresses: joi.array() .items(serviceIngressSchema) .description("List of currently deployed ingress endpoints for the service."), - lastMessage: Joi.string() + lastMessage: joi.string() .allow("") .description("Latest status message of the service (if any)."), - lastError: Joi.string() + lastError: joi.string() .description("Latest error status message of the service (if any)."), - createdAt: Joi.string() + createdAt: joi.string() .description("When the service was first deployed by the provider."), - updatedAt: Joi.string() + updatedAt: joi.string() .description("When the service was last updated by the provider."), - detail: Joi.object() + detail: joi.object() .meta({ extendable: true }) .description("Additional detail, specific to the provider."), }) @@ -192,17 +191,17 @@ export type RuntimeContext = { }, } -const runtimeDependencySchema = Joi.object() +const runtimeDependencySchema = joi.object() .keys({ version: moduleVersionSchema, outputs: joiEnvVars() .description("The outputs provided by the service (e.g. ingress URLs etc.)."), }) -export const runtimeContextSchema = Joi.object() +export const runtimeContextSchema = joi.object() .options({ presence: "required" }) .keys({ - envVars: Joi.object().pattern(/.+/, joiPrimitive()) + envVars: joi.object().pattern(/.+/, joiPrimitive()) .default(() => ({}), "{}") .unknown(false) .description( diff --git a/garden-service/src/vcs/vcs.ts b/garden-service/src/vcs/vcs.ts index 03f9171e9f..827816d204 100644 --- a/garden-service/src/vcs/vcs.ts +++ b/garden-service/src/vcs/vcs.ts @@ -9,8 +9,7 @@ import * as Bluebird from "bluebird" import { mapValues, keyBy, sortBy, omit } from "lodash" import { createHash } from "crypto" -import * as Joi from "joi" -import { validate, joiArray } from "../config/common" +import { validate, joiArray, joi } from "../config/common" import { join } from "path" import { GARDEN_VERSIONFILE_NAME } from "../constants" import { pathExists, readFile, writeFile } from "fs-extra" @@ -38,26 +37,26 @@ interface NamedTreeVersion extends TreeVersion { name: string } -const versionStringSchema = Joi.string() +const versionStringSchema = joi.string() .regex(/^v/) .required() .description("String representation of the module version.") -const fileNamesSchema = joiArray(Joi.string()) +const fileNamesSchema = joiArray(joi.string()) .description("List of file paths included in the version.") -export const treeVersionSchema = Joi.object() +export const treeVersionSchema = joi.object() .keys({ - contentHash: Joi.string() + contentHash: joi.string() .required() .description("The hash of all files in the directory, after filtering."), files: fileNamesSchema, }) -export const moduleVersionSchema = Joi.object() +export const moduleVersionSchema = joi.object() .keys({ versionString: versionStringSchema, - dependencyVersions: Joi.object() + dependencyVersions: joi.object() .pattern(/.+/, treeVersionSchema) .default(() => ({}), "{}") .description("The version of each of the dependencies of the module."), diff --git a/garden-service/test/helpers.ts b/garden-service/test/helpers.ts index 6249527111..63af034b07 100644 --- a/garden-service/test/helpers.ts +++ b/garden-service/test/helpers.ts @@ -7,14 +7,13 @@ */ import * as td from "testdouble" -import * as Joi from "joi" import { resolve, join } from "path" import { extend } from "lodash" import { remove, readdirSync, existsSync } from "fs-extra" import { containerModuleSpecSchema, containerTestSchema, containerTaskSchema } from "../src/plugins/container/config" import { testExecModule, buildExecModule, execBuildSpecSchema } from "../src/plugins/exec" import { TaskResults } from "../src/task-graph" -import { validate, joiArray } from "../src/config/common" +import { validate, joiArray, joi } from "../src/config/common" import { GardenPlugin, PluginActions, @@ -80,10 +79,10 @@ async function runModule(params: RunModuleParams): Promise { export const projectRootA = getDataDir("test-project-a") const testModuleTestSchema = containerTestSchema - .keys({ command: Joi.array().items(Joi.string()) }) + .keys({ command: joi.array().items(joi.string()) }) const testModuleTaskSchema = containerTaskSchema - .keys({ command: Joi.array().items(Joi.string()) }) + .keys({ command: joi.array().items(joi.string()) }) export const testModuleSpecSchema = containerModuleSpecSchema .keys({ @@ -328,11 +327,13 @@ export function stubModuleAction>( return td.replace(garden["moduleActionHandlers"][actionType][moduleType], pluginName, handler) } -export async function expectError(fn: Function, typeOrCallback: string | ((err: any) => void)) { +export async function expectError(fn: Function, typeOrCallback?: string | ((err: any) => void)) { try { await fn() } catch (err) { - if (typeof typeOrCallback === "function") { + if (typeOrCallback === undefined) { + return + } else if (typeof typeOrCallback === "function") { return typeOrCallback(err) } else { if (!err.type) { diff --git a/garden-service/test/unit/src/actions.ts b/garden-service/test/unit/src/actions.ts index d535f3b0a1..379286dac6 100644 --- a/garden-service/test/unit/src/actions.ts +++ b/garden-service/test/unit/src/actions.ts @@ -1,4 +1,3 @@ -import * as Joi from "joi" import { ModuleAndRuntimeActions, @@ -19,7 +18,7 @@ import Stream from "ts-stream" import { Task } from "../../../src/types/task" import { expect } from "chai" import { omit } from "lodash" -import { validate } from "../../../src/config/common" +import { validate, joi } from "../../../src/config/common" const now = new Date() @@ -429,8 +428,8 @@ const testPlugin: PluginFactory = async () => ({ validate(params, moduleActionDescriptions.describeType.paramsSchema) return { docs: "bla bla bla", - outputsSchema: Joi.object(), - schema: Joi.object(), + outputsSchema: joi.object(), + schema: joi.object(), title: "Bla", } }, diff --git a/garden-service/test/unit/src/config/common.ts b/garden-service/test/unit/src/config/common.ts index f3762b34b8..0194e60975 100644 --- a/garden-service/test/unit/src/config/common.ts +++ b/garden-service/test/unit/src/config/common.ts @@ -1,7 +1,6 @@ import { expect } from "chai" -import * as Joi from "joi" const stripAnsi = require("strip-ansi") -import { identifierRegex, validate, envVarRegex, userIdentifierRegex } from "../../../../src/config/common" +import { identifierRegex, validate, envVarRegex, userIdentifierRegex, joi } from "../../../../src/config/common" import { expectError } from "../../../helpers" describe("envVarRegex", () => { @@ -114,15 +113,15 @@ describe("validate", () => { my: "object", } - validate(obj, Joi.object().keys({ my: Joi.string() })) + validate(obj, joi.object().keys({ my: joi.string() })) }) it("should throw a nice error when keys are missing", async () => { const obj = { B: {} } - const schema = Joi.object().keys({ - A: Joi.string().required(), - B: Joi.object().keys({ - b: Joi.string().required(), + const schema = joi.object().keys({ + A: joi.string().required(), + B: joi.object().keys({ + b: joi.string().required(), }).required(), }) @@ -133,10 +132,10 @@ describe("validate", () => { it("should throw a nice error when keys are wrong in a pattern object", async () => { const obj = { A: { B: { c: {} } } } - const schema = Joi.object().keys({ - A: Joi.object().keys({ - B: Joi.object().pattern(/.+/, Joi.object().keys({ - C: Joi.string().required(), + const schema = joi.object().keys({ + A: joi.object().keys({ + B: joi.object().pattern(/.+/, joi.object().keys({ + C: joi.string().required(), })).required(), }).required(), }) @@ -148,7 +147,7 @@ describe("validate", () => { it("should throw a nice error when key is invalid", async () => { const obj = { 123: "abc" } - const schema = Joi.object().pattern(/[a-z]+/, Joi.string()) + const schema = joi.object().pattern(/[a-z]+/, joi.string()) await expectError(() => validate(obj, schema), (err) => { expect(stripAnsi(err.detail.errorDescription)).to.equal("key \"123\" is not allowed at path .") @@ -157,7 +156,7 @@ describe("validate", () => { it("should throw a nice error when nested key is invalid", async () => { const obj = { a: { 123: "abc" } } - const schema = Joi.object().keys({ a: Joi.object().pattern(/[a-z]+/, Joi.string()) }) + const schema = joi.object().keys({ a: joi.object().pattern(/[a-z]+/, joi.string()) }) await expectError(() => validate(obj, schema), (err) => { expect(stripAnsi(err.detail.errorDescription)).to.equal("key \"123\" is not allowed at path .a") @@ -166,9 +165,9 @@ describe("validate", () => { it("should throw a nice error when xor rule fails", async () => { const obj = { a: 1, b: 2 } - const schema = Joi.object().keys({ - a: Joi.number(), - b: Joi.number(), + const schema = joi.object().keys({ + a: joi.number(), + b: joi.number(), }).xor("a", "b") await expectError(() => validate(obj, schema), (err) => { @@ -176,3 +175,66 @@ describe("validate", () => { }) }) }) + +describe("joi.posixPath", () => { + it("should validate a POSIX-style path", () => { + const path = "/foo/bar.js" + const schema = joi.string().posixPath() + const result = schema.validate(path) + expect(result.error).to.be.null + }) + + it("should return error with a Windows-style path", () => { + const path = "C:\\Something\\Blorg" + const schema = joi.string().posixPath() + const result = schema.validate(path) + expect(result.error).to.exist + }) + + it("should error if attempting to set absoluteOnly and relativeOnly at same time", async () => { + return expectError( + () => joi.string().posixPath({ absoluteOnly: true, relativeOnly: true }), + ) + }) + + it("should error if attempting to set absoluteOnly and subPathOnly at same time", async () => { + return expectError( + () => joi.string().posixPath({ absoluteOnly: true, subPathOnly: true }), + ) + }) + + it("should respect absoluteOnly parameter", () => { + const path = "foo/bar.js" + const schema = joi.string().posixPath({ absoluteOnly: true }) + const result = schema.validate(path) + expect(result.error).to.exist + }) + + it("should respect relativeOnly parameter", () => { + const path = "/foo/bar.js" + const schema = joi.string().posixPath({ relativeOnly: true }) + const result = schema.validate(path) + expect(result.error).to.exist + }) + + it("should respect subPathOnly parameter by rejecting absolute paths", () => { + const path = "/foo/bar.js" + const schema = joi.string().posixPath({ subPathOnly: true }) + const result = schema.validate(path) + expect(result.error).to.exist + }) + + it("should respect subPathOnly parameter by rejecting paths with '..' segments", () => { + const path = "foo/../../bar" + const schema = joi.string().posixPath({ subPathOnly: true }) + const result = schema.validate(path) + expect(result.error).to.exist + }) + + it("should allow paths with '..' segments when subPathOnly=false", () => { + const path = "foo/../../bar" + const schema = joi.string().posixPath() + const result = schema.validate(path) + expect(result.error).to.be.null + }) +}) diff --git a/garden-service/test/unit/src/config/config-context.ts b/garden-service/test/unit/src/config/config-context.ts index 36bb0f3a87..cb2063baa7 100644 --- a/garden-service/test/unit/src/config/config-context.ts +++ b/garden-service/test/unit/src/config/config-context.ts @@ -8,9 +8,9 @@ import { ModuleConfigContext, } from "../../../../src/config/config-context" import { expectError, makeTestGardenA } from "../../../helpers" -import * as Joi from "joi" import { Garden } from "../../../../src/garden" import { join } from "path" +import { joi } from "../../../../src/config/common" type TestValue = string | ConfigContext | TestValues | TestValueFunction type TestValueFunction = () => TestValue | Promise @@ -165,12 +165,12 @@ describe("ConfigContext", () => { describe("getSchema", () => { it("should return a Joi object schema with all described attributes", () => { class Nested extends ConfigContext { - @schema(Joi.string().description("Nested description")) + @schema(joi.string().description("Nested description")) nestedKey: string } class Context extends ConfigContext { - @schema(Joi.string().description("Some description")) + @schema(joi.string().description("Some description")) key: string @schema(Nested.getSchema().description("A nested context")) diff --git a/garden-service/test/unit/src/docs/config.ts b/garden-service/test/unit/src/docs/config.ts index d8ff50ffff..fafcca3e2d 100644 --- a/garden-service/test/unit/src/docs/config.ts +++ b/garden-service/test/unit/src/docs/config.ts @@ -5,24 +5,23 @@ import { renderConfigReference, } from "../../../../src/docs/config" import { expect } from "chai" -import * as Joi from "joi" import dedent = require("dedent") -import { joiArray } from "../../../../src/config/common" +import { joiArray, joi } from "../../../../src/config/common" describe("config", () => { - const serivcePortSchema = Joi.number().default((context) => context.containerPort, "") + const serivcePortSchema = joi.number().default((context) => context.containerPort, "") .example("8080") .description("description") - const testDefaultSchema = Joi.number().default(() => "result", "default value") + const testDefaultSchema = joi.number().default(() => "result", "default value") .description("description") - const testObject = Joi.object() + const testObject = joi.object() .keys({ - testKeyA: Joi.number() + testKeyA: joi.number() .required() .description("key a"), - testKeyB: Joi.string() + testKeyB: joi.string() .only("b") .description("key b"), }) @@ -31,9 +30,9 @@ describe("config", () => { const testArray = joiArray(serivcePortSchema) .description("test array") - const portSchema = Joi.object() + const portSchema = joi.object() .keys({ - containerPort: Joi.number() + containerPort: joi.number() .required() .description("description"), servicePort: serivcePortSchema, diff --git a/garden-service/test/unit/src/garden.ts b/garden-service/test/unit/src/garden.ts index e38cf85dae..4e9fc3625f 100644 --- a/garden-service/test/unit/src/garden.ts +++ b/garden-service/test/unit/src/garden.ts @@ -27,10 +27,10 @@ import { ConfigureProviderParams } from "../../../src/types/plugin/provider/conf import { ProjectConfig } from "../../../src/config/project" import { ModuleConfig } from "../../../src/config/module" import { DEFAULT_API_VERSION } from "../../../src/constants" -import * as Joi from "joi" import { providerConfigBaseSchema } from "../../../src/config/provider" import { keyBy } from "lodash" import stripAnsi from "strip-ansi" +import { joi } from "../../../src/config/common" describe("Garden", () => { beforeEach(async () => { @@ -577,7 +577,7 @@ describe("Garden", () => { return { configSchema: providerConfigBaseSchema .keys({ - foo: Joi.string().default("bar"), + foo: joi.string().default("bar"), }), } } @@ -610,7 +610,7 @@ describe("Garden", () => { return { configSchema: providerConfigBaseSchema .keys({ - foo: Joi.string(), + foo: joi.string(), }), } }