From 4b188ee7bf543f99d2353bb8d20d54e0ed03d5bf Mon Sep 17 00:00:00 2001 From: Jon Edvald Date: Tue, 5 Nov 2019 16:18:23 +0100 Subject: [PATCH] improvement(openfaas): updated faas-netes and made more configurable See the updated docs for details. Fixes #1306 --- docs/reference/module-types/openfaas.md | 4 +- docs/reference/providers/local-openfaas.md | 60 ++++++- docs/reference/providers/openfaas.md | 60 ++++++- garden-service/src/plugins/openfaas/config.ts | 47 ++++- .../src/plugins/openfaas/faas-cli.ts | 12 +- .../src/plugins/openfaas/openfaas.ts | 167 +++++++++++------- 6 files changed, 274 insertions(+), 76 deletions(-) diff --git a/docs/reference/module-types/openfaas.md b/docs/reference/module-types/openfaas.md index 647e564218..b061f8cc62 100644 --- a/docs/reference/module-types/openfaas.md +++ b/docs/reference/module-types/openfaas.md @@ -4,8 +4,8 @@ title: Openfaas # `openfaas` reference -Deploy [OpenFaaS](https://www.openfaas.com/) functions using Garden. Requires either the `openfaas` or -`local-openfaas` provider to be configured. +Deploy [OpenFaaS](https://www.openfaas.com/) functions using Garden. Requires the `openfaas` provider +to be configured. Below is the schema reference. For an introduction to configuring Garden modules, please look at our [Configuration guide](../../guides/configuration-files.md). diff --git a/docs/reference/providers/local-openfaas.md b/docs/reference/providers/local-openfaas.md index f3541ea922..c5a200fd76 100644 --- a/docs/reference/providers/local-openfaas.md +++ b/docs/reference/providers/local-openfaas.md @@ -52,12 +52,30 @@ providers: - name: "openfaas" ``` +### `providers[].gatewayUrl` + +[providers](#providers) > gatewayUrl + +The external URL to the function gateway, after installation. This is required if you set `faasNetes.values` +or `faastNetes.install: false`, so that Garden can know how to reach the gateway. + +| Type | Required | +| -------- | -------- | +| `string` | No | + +Example: + +```yaml +providers: + - gatewayUrl: "https://functions.mydomain.com" +``` + ### `providers[].hostname` [providers](#providers) > hostname -The hostname to configure for the function gateway. -Defaults to the default hostname of the configured Kubernetes provider. +The ingress hostname to configure for the function gateway, when `faasNetes.install: true` and not +overriding `faasNetes.values`. Defaults to the default hostname of the configured Kubernetes provider. Important: If you have other types of services, this should be different from their ingress hostnames, or the other services should not expose paths under /function and /system to avoid routing conflicts. @@ -73,6 +91,40 @@ providers: - hostname: "functions.mydomain.com" ``` +### `providers[].faasNetes` + +[providers](#providers) > faasNetes + +| Type | Required | Default | +| -------- | -------- | ------------------ | +| `object` | No | `{"install":true}` | + +### `providers[].faasNetes.install` + +[providers](#providers) > [faasNetes](#providersfaasnetes) > install + +Set to false if you'd like to install and configure faas-netes yourself. +See the [official instructions](https://docs.openfaas.com/deployment/kubernetes/) for details. + +| Type | Required | Default | +| --------- | -------- | ------- | +| `boolean` | No | `true` | + +### `providers[].faasNetes.values` + +[providers](#providers) > [faasNetes](#providersfaasnetes) > values + +Override the values passed to the faas-netes Helm chart. Ignored if `install: false`. +See the [chart docs](https://github.com/openfaas/faas-netes/tree/master/chart/openfaas) for the available +options. + +Note that this completely replaces the values Garden assigns by default, including `functionNamespace`, +ingress configuration etc. so you need to make sure those are correctly configured for your use case. + +| Type | Required | +| -------- | -------- | +| `object` | No | + ## Complete YAML schema @@ -82,5 +134,9 @@ The values in the schema below are the default values. providers: - environments: name: openfaas + gatewayUrl: hostname: + faasNetes: + install: true + values: ``` diff --git a/docs/reference/providers/openfaas.md b/docs/reference/providers/openfaas.md index 5a0184a888..f84d71a369 100644 --- a/docs/reference/providers/openfaas.md +++ b/docs/reference/providers/openfaas.md @@ -52,12 +52,30 @@ providers: - name: "openfaas" ``` +### `providers[].gatewayUrl` + +[providers](#providers) > gatewayUrl + +The external URL to the function gateway, after installation. This is required if you set `faasNetes.values` +or `faastNetes.install: false`, so that Garden can know how to reach the gateway. + +| Type | Required | +| -------- | -------- | +| `string` | No | + +Example: + +```yaml +providers: + - gatewayUrl: "https://functions.mydomain.com" +``` + ### `providers[].hostname` [providers](#providers) > hostname -The hostname to configure for the function gateway. -Defaults to the default hostname of the configured Kubernetes provider. +The ingress hostname to configure for the function gateway, when `faasNetes.install: true` and not +overriding `faasNetes.values`. Defaults to the default hostname of the configured Kubernetes provider. Important: If you have other types of services, this should be different from their ingress hostnames, or the other services should not expose paths under /function and /system to avoid routing conflicts. @@ -73,6 +91,40 @@ providers: - hostname: "functions.mydomain.com" ``` +### `providers[].faasNetes` + +[providers](#providers) > faasNetes + +| Type | Required | Default | +| -------- | -------- | ------------------ | +| `object` | No | `{"install":true}` | + +### `providers[].faasNetes.install` + +[providers](#providers) > [faasNetes](#providersfaasnetes) > install + +Set to false if you'd like to install and configure faas-netes yourself. +See the [official instructions](https://docs.openfaas.com/deployment/kubernetes/) for details. + +| Type | Required | Default | +| --------- | -------- | ------- | +| `boolean` | No | `true` | + +### `providers[].faasNetes.values` + +[providers](#providers) > [faasNetes](#providersfaasnetes) > values + +Override the values passed to the faas-netes Helm chart. Ignored if `install: false`. +See the [chart docs](https://github.com/openfaas/faas-netes/tree/master/chart/openfaas) for the available +options. + +Note that this completely replaces the values Garden assigns by default, including `functionNamespace`, +ingress configuration etc. so you need to make sure those are correctly configured for your use case. + +| Type | Required | +| -------- | -------- | +| `object` | No | + ## Complete YAML schema @@ -82,5 +134,9 @@ The values in the schema below are the default values. providers: - environments: name: openfaas + gatewayUrl: hostname: + faasNetes: + install: true + values: ``` diff --git a/garden-service/src/plugins/openfaas/config.ts b/garden-service/src/plugins/openfaas/config.ts index 732fcd0b12..3bc075616a 100644 --- a/garden-service/src/plugins/openfaas/config.ts +++ b/garden-service/src/plugins/openfaas/config.ts @@ -11,7 +11,7 @@ import { join } from "path" import { resolve as urlResolve } from "url" import { ConfigurationError } from "../../exceptions" import { PluginContext } from "../../plugin-context" -import { joiArray, joiProviderName, joi, joiEnvVars } from "../../config/common" +import { joiArray, joiProviderName, joi, joiEnvVars, DeepPrimitiveMap } from "../../config/common" import { Module } from "../../types/module" import { Service } from "../../types/service" import { ExecModuleSpecBase, ExecTestSpec, execTestSchema } from "../exec" @@ -81,24 +81,56 @@ export type OpenFaasModuleConfig = OpenFaasModule["_ConfigType"] export interface OpenFaasService extends Service {} export interface OpenFaasConfig extends ProviderConfig { + gatewayUrl: string hostname: string + faasNetes: { + install: boolean + values: DeepPrimitiveMap + } } export const configSchema = providerConfigBaseSchema.keys({ name: joiProviderName("openfaas"), + gatewayUrl: joi + .string() + .uri({ scheme: ["http", "https"] }) + .description( + dedent` + The external URL to the function gateway, after installation. This is required if you set \`faasNetes.values\` + or \`faastNetes.install: false\`, so that Garden can know how to reach the gateway. + ` + ) + .example("https://functions.mydomain.com"), hostname: joi .string() .hostname() - .allow(null) .description( dedent` - The hostname to configure for the function gateway. - Defaults to the default hostname of the configured Kubernetes provider. + The ingress hostname to configure for the function gateway, when \`faasNetes.install: true\` and not + overriding \`faasNetes.values\`. Defaults to the default hostname of the configured Kubernetes provider. Important: If you have other types of services, this should be different from their ingress hostnames, - or the other services should not expose paths under /function and /system to avoid routing conflicts.` + or the other services should not expose paths under /function and /system to avoid routing conflicts. + ` ) .example("functions.mydomain.com"), + faasNetes: joi + .object() + .keys({ + install: joi.boolean().default(true).description(dedent` + Set to false if you'd like to install and configure faas-netes yourself. + See the [official instructions](https://docs.openfaas.com/deployment/kubernetes/) for details. + `), + values: joi.object().description(dedent` + Override the values passed to the faas-netes Helm chart. Ignored if \`install: false\`. + See the [chart docs](https://github.com/openfaas/faas-netes/tree/master/chart/openfaas) for the available + options. + + Note that this completely replaces the values Garden assigns by default, including \`functionNamespace\`, + ingress configuration etc. so you need to make sure those are correctly configured for your use case. + `), + }) + .default({ install: true }), }) export type OpenFaasProvider = Provider @@ -106,6 +138,7 @@ export type OpenFaasPluginContext = PluginContext export function getK8sProvider(providers: Provider[]): KubernetesProvider { const providerMap = keyBy(providers, "name") + // FIXME: use new plugin inheritance mechanism here, instead of explicitly checking for local-kubernetes const provider = (providerMap["local-kubernetes"] || providerMap.kubernetes) if (!provider) { @@ -212,3 +245,7 @@ async function getInternalServiceUrl(ctx: PluginContext, log: Lo export function getServicePath(config: OpenFaasModuleConfig) { return join("/", "function", config.name) } + +export function getExternalGatewayUrl(ctx: PluginContext) { + return ctx.provider.config.gatewayUrl || `http://${ctx.provider.config.hostname}` +} diff --git a/garden-service/src/plugins/openfaas/faas-cli.ts b/garden-service/src/plugins/openfaas/faas-cli.ts index e25136bf0a..92530d29e0 100644 --- a/garden-service/src/plugins/openfaas/faas-cli.ts +++ b/garden-service/src/plugins/openfaas/faas-cli.ts @@ -12,16 +12,16 @@ export const faasCli = new BinaryCmd({ name: "faas-cli", specs: { darwin: { - url: "https://github.com/openfaas/faas-cli/releases/download/0.8.21/faas-cli-darwin", - sha256: "68d99f789e2e0a763b6f58f075f0118b8828fd43b3ca4eed646961eb6ac352fa", + url: "https://github.com/openfaas/faas-cli/releases/download/0.9.5/faas-cli-darwin", + sha256: "28beff63ef8234c1c937b14fd63e8c25244432897830650b8f76897fe4e22cbb", }, linux: { - url: "https://github.com/openfaas/faas-cli/releases/download/0.8.21/faas-cli", - sha256: "b8a5b455f20b14751140cb63277ee4d435e23ed041be1898a0dc2c27ee718046", + url: "https://github.com/openfaas/faas-cli/releases/download/0.9.5/faas-cli", + sha256: "f4c8014d953f42e0c83628c089aff36aaf306f9f1aea62e5f22c84ab4269d1f7", }, win32: { - url: "https://github.com/openfaas/faas-cli/releases/download/0.8.21/faas-cli.exe", - sha256: "366e01a364e64f90bec6b8234c2bc5bb87bbd059b187f8afe43c36d22f4d5b84", + url: "https://github.com/openfaas/faas-cli/releases/download/0.9.5/faas-cli.exe", + sha256: "45d09e4dbff679c32aff8f86cc39e12c3687b6b344a9a20510c6c61f4e141eb5", }, }, }) diff --git a/garden-service/src/plugins/openfaas/openfaas.ts b/garden-service/src/plugins/openfaas/openfaas.ts index 35ef38553c..546f171c58 100644 --- a/garden-service/src/plugins/openfaas/openfaas.ts +++ b/garden-service/src/plugins/openfaas/openfaas.ts @@ -8,9 +8,9 @@ import { join } from "path" import { ConfigurationError } from "../../exceptions" -import { ServiceStatus, ServiceIngress } from "../../types/service" +import { ServiceStatus, ServiceIngress, ServiceProtocol } from "../../types/service" import { testExecModule } from "../exec" -import { getNamespace, getAppNamespace } from "../kubernetes/namespace" +import { getNamespace } from "../kubernetes/namespace" import { findByName } from "../../util/util" import { KubeApi } from "../kubernetes/api" import { waitForResources } from "../kubernetes/status/status" @@ -39,9 +39,14 @@ import { configureModule, openfaasModuleOutputsSchema, openfaasModuleSpecSchema, + getExternalGatewayUrl, } from "./config" import { getOpenfaasModuleBuildStatus, buildOpenfaasModule, writeStackFile, stackFilename } from "./build" import { dedent } from "../../util/string" +import { LogEntry } from "../../logger/log-entry" +import { Provider } from "../../config/provider" +import { parse } from "url" +import { trim } from "lodash" const systemDir = join(STATIC_DIR, "openfaas", "system") @@ -56,8 +61,8 @@ export const gardenPlugin = createGardenPlugin({ { name: "openfaas", docs: dedent` - Deploy [OpenFaaS](https://www.openfaas.com/) functions using Garden. Requires either the \`openfaas\` or - \`local-openfaas\` provider to be configured. + Deploy [OpenFaaS](https://www.openfaas.com/) functions using Garden. Requires the \`openfaas\` provider + to be configured. `, moduleOutputsSchema: openfaasModuleOutputsSchema, schema: openfaasModuleSpecSchema, @@ -85,7 +90,7 @@ const templateModuleConfig: ExecModuleConfig = { description: "OpenFaaS templates for building functions", name: "templates", path: join(systemDir, "openfaas-templates"), - repositoryUrl: "https://github.com/openfaas/templates.git#master", + repositoryUrl: "https://github.com/openfaas/templates.git#1.2", outputs: {}, serviceConfigs: [], spec: { @@ -110,28 +115,63 @@ async function configureProvider({ }: ConfigureProviderParams): Promise { const k8sProvider = getK8sProvider(dependencies) - if (!config.hostname) { - if (!k8sProvider.config.defaultHostname) { + if (!config.faasNetes.install || config.faasNetes.values) { + // The user is manually configuring faas-netes + if (!config.gatewayUrl) { throw new ConfigurationError( - `openfaas: Must configure hostname if no default hostname is configured on Kubernetes provider.`, + `openfaas: gatewayUrl field must be configured when manually configuring or installing faas-netes.`, { config } ) } - - config.hostname = k8sProvider.config.defaultHostname + } else { + // We set a basic default configuration + if (!config.hostname) { + if (!k8sProvider.config.defaultHostname) { + throw new ConfigurationError( + `openfaas: hostname field must be configured if no default hostname is configured on Kubernetes provider.`, + { config } + ) + } + + config.hostname = k8sProvider.config.defaultHostname + } } - const namespace = await getNamespace({ - log, - provider: k8sProvider, - projectName, - skipCreate: true, - }) - + const namespace = await getFunctionNamespace(log, projectName, config, dependencies) // Need to scope the release name, because the OpenFaaS Helm chart installs some cluster-wide resources // that could conflict across projects/users. const releaseName = `${namespace}--openfaas` + const defaultValues = { + basic_auth: false, + exposeServices: false, + functionNamespace: namespace, + ingress: { + enabled: true, + hosts: [ + { + host: config.hostname, + serviceName: "gateway", + servicePort: 8080, + path: "/function/", + }, + { + host: config.hostname, + serviceName: "gateway", + servicePort: 8080, + path: "/system/", + }, + ], + }, + faasIdler: { + create: false, + }, + faasnetes: { + imagePullPolicy: "IfNotPresent", + }, + securityContext: false, + } + const systemModule: HelmModuleConfig = { allowPublish: false, apiVersion: DEFAULT_API_VERSION, @@ -155,39 +195,9 @@ async function configureProvider({ tasks: [], tests: [], timeout: 900, - version: "4.4.0", + version: "5.2.1", releaseName, - values: { - // TODO: allow setting password in provider config - basic_auth: false, - exposeServices: false, - functionNamespace: namespace, - ingress: { - enabled: true, - hosts: [ - { - host: config.hostname, - serviceName: "gateway", - servicePort: 8080, - path: "/function/", - }, - { - host: config.hostname, - serviceName: "gateway", - servicePort: 8080, - path: "/system/", - }, - ], - }, - // TODO: make this (and more stuff) configurable - faasIdler: { - create: false, - }, - faasnetes: { - imagePullPolicy: "IfNotPresent", - }, - securityContext: false, - }, + values: config.faasNetes.values || defaultValues, valueFiles: [], }, } @@ -197,15 +207,39 @@ async function configureProvider({ return { config, moduleConfigs } } +async function getFunctionNamespace( + log: LogEntry, + projectName: string, + config: OpenFaasConfig, + dependencies: Provider[] +) { + // Check for configured namespace in faas-netes custom values + return ( + (config.values && config.values.functionNamespace) || + // Default to K8s app namespace + (await getNamespace({ + log, + provider: getK8sProvider(dependencies), + projectName, + skipCreate: true, + })) + ) +} + async function getServiceLogs(params: GetServiceLogsParams) { const { ctx, log, service } = params - const provider = getK8sProvider(ctx.provider.dependencies) - const namespace = await getAppNamespace(ctx, log, provider) + const namespace = await getFunctionNamespace( + log, + ctx.projectName, + ctx.provider.config as OpenFaasConfig, + ctx.provider.dependencies + ) - const api = await KubeApi.factory(log, provider) + const k8sProvider = getK8sProvider(ctx.provider.dependencies) + const api = await KubeApi.factory(log, k8sProvider) const resources = await getResources(api, service, namespace) - return getAllLogs({ ...params, provider, defaultNamespace: namespace, resources }) + return getAllLogs({ ...params, provider: k8sProvider, defaultNamespace: namespace, resources }) } async function deployService(params: DeployServiceParams): Promise { @@ -224,7 +258,12 @@ async function deployService(params: DeployServiceParams): Promi }) // wait until deployment is ready - const namespace = await getAppNamespace(ctx, log, k8sProvider) + const namespace = await getFunctionNamespace( + log, + ctx.projectName, + ctx.provider.config as OpenFaasConfig, + ctx.provider.dependencies + ) const api = await KubeApi.factory(log, k8sProvider) const resources = await getResources(api, service, namespace) @@ -242,7 +281,7 @@ async function deployService(params: DeployServiceParams): Promi async function deleteService(params: DeleteServiceParams): Promise { const { ctx, log, service } = params - let status + let status: ServiceStatus let found = true try { @@ -267,6 +306,7 @@ async function deleteService(params: DeleteServiceParams): Promi }) } catch (err) { found = false + status = { state: "missing", detail: {} } } if (log) { @@ -290,16 +330,25 @@ async function getServiceStatus({ const openFaasCtx = ctx const k8sProvider = getK8sProvider(ctx.provider.dependencies) + const gatewayUrl = getExternalGatewayUrl(openFaasCtx) + const parsed = parse(gatewayUrl) + const protocol = trim(parsed.protocol!, ":") as ServiceProtocol + const ingresses: ServiceIngress[] = [ { - hostname: ctx.provider.config.hostname, + hostname: parsed.hostname!, path: getServicePath(module), - port: k8sProvider.config.ingressHttpPort, - protocol: "http", + port: protocol === "https" ? 443 : 80, + protocol, }, ] - const namespace = await getAppNamespace(openFaasCtx, log, k8sProvider) + const namespace = await getFunctionNamespace( + log, + openFaasCtx.projectName, + openFaasCtx.provider.config, + openFaasCtx.provider.dependencies + ) const api = await KubeApi.factory(log, k8sProvider) let deployment: KubernetesDeployment