Skip to content

Commit

Permalink
feat(openfaas): enable remote building for openfaas modules
Browse files Browse the repository at this point in the history
Also updated the OpenFaaS components. I'll follow up with another
commit to make the configuration of the OpenFaaS system components
more flexible.

NOTE: This does not enable integration testing for OpenFaaS because
of an issue with deploying the faas-netes Helm chart (see issue #1045)
  • Loading branch information
edvald authored and eysi09 committed Jul 30, 2019
1 parent cd77a76 commit a0d913d
Show file tree
Hide file tree
Showing 13 changed files with 329 additions and 235 deletions.
17 changes: 11 additions & 6 deletions examples/openfaas/garden.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
kind: Project
name: openfaas
environmentDefaults:
providers:
- name: npm-package
variables:
my-variable: hello-variable
environments:
- name: local
providers:
- name: local-kubernetes
- name: local-openfaas

hostname: openfaas-gateway.local.app.garden
- name: testing
providers:
- name: kubernetes
context: gke_garden-dev-200012_europe-west1-b_garden-dev-1
namespace: openfaas-testing-${local.env.CIRCLE_BUILD_NUM || local.username}
defaultHostname: openfaas-testing-${local.env.CIRCLE_BUILD_NUM || local.username}.dev-1.sys.garden
buildMode: cluster-docker
- name: openfaas
variables:
my-variable: hello-variable
2 changes: 1 addition & 1 deletion examples/openfaas/libraries/hello-npm-package/garden.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
kind: Module
description: Hello world npm package
type: npm-package
type: exec
name: hello-npm-package
2 changes: 1 addition & 1 deletion garden-service/src/docs/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { baseModuleSpecSchema } from "../config/module"
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 { configSchema as openfaasConfigSchema } from "../plugins/openfaas/config"
import { joiArray, joi } from "../config/common"
import { mavenContainerConfigSchema } from "../plugins/maven-container/maven-container"
import { Garden } from "../garden"
Expand Down
4 changes: 2 additions & 2 deletions garden-service/src/plugins/container/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ export async function buildContainerModule({ module, log }: BuildModuleParams<Co

// TODO: log error if it occurs
// TODO: stream output to log if at debug log level
await containerHelpers.dockerCli(module, [...cmdOpts, buildPath])
const buildLog = await containerHelpers.dockerCli(module, [...cmdOpts, buildPath])

return { fresh: true, details: { identifier } }
return { fresh: true, buildLog, details: { identifier } }
}

export function getDockerBuildFlags(module: ContainerModule) {
Expand Down
3 changes: 2 additions & 1 deletion garden-service/src/plugins/container/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ const helpers = {
* Returns the image ID to be used when pushing to deployment registries. This always has the module version
* set as the tag.
*/
async getDeploymentImageId(module: ContainerModule, registryConfig?: ContainerRegistryConfig) {
// Requiring 2nd parameter to avoid accidentally missing it
async getDeploymentImageId(module: ContainerModule, registryConfig: ContainerRegistryConfig | undefined) {
if (await helpers.hasDockerfile(module)) {
// If building, return the deployment image name, with the current module version.
const imageName = await helpers.getDeploymentImageName(module, registryConfig)
Expand Down
75 changes: 75 additions & 0 deletions garden-service/src/plugins/openfaas/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2018 Garden Technologies, Inc. <info@garden.io>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { join } from "path"
import { PrimitiveMap } from "../../config/common"
import { KubernetesProvider } from "../kubernetes/config"
import { dumpYaml } from "../../util/util"
import { faasCli } from "./faas-cli"
import { BuildModuleParams } from "../../types/plugin/module/build"
import { containerHelpers } from "../container/helpers"
import { k8sBuildContainer, k8sGetContainerBuildStatus } from "../kubernetes/container/build"
import { GetBuildStatusParams } from "../../types/plugin/module/getBuildStatus"
import { OpenFaasModule, getK8sProvider, getContainerModule, OpenFaasProvider } from "./config"

export const stackFilename = "stack.yml"

export async function getOpenfaasModuleBuildStatus({ ctx, log, module }: GetBuildStatusParams<OpenFaasModule>) {
const containerModule = getContainerModule(module)
const k8sProvider = getK8sProvider(ctx.provider.dependencies)
const k8sCtx = { ...ctx, provider: k8sProvider }
return k8sGetContainerBuildStatus({ ctx: k8sCtx, log, module: containerModule })
}

export async function buildOpenfaasModule({ ctx, log, module }: BuildModuleParams<OpenFaasModule>) {
const k8sProvider = getK8sProvider(ctx.provider.dependencies)
await writeStackFile(<OpenFaasProvider>ctx.provider, k8sProvider, module, {})

const buildLog = await faasCli.stdout({
log,
cwd: module.buildPath,
args: ["build", "--shrinkwrap", "-f", stackFilename],
})

const containerModule = getContainerModule(module)
const k8sCtx = { ...ctx, provider: k8sProvider }
const result = await k8sBuildContainer({ ctx: k8sCtx, log, module: containerModule })

return { fresh: true, buildLog: buildLog + "\n" + result.buildLog }
}

export async function writeStackFile(
provider: OpenFaasProvider, k8sProvider: KubernetesProvider, module: OpenFaasModule, envVars: PrimitiveMap,
) {
const containerModule = getContainerModule(module)
const image = await containerHelpers.getDeploymentImageId(containerModule, k8sProvider.config.deploymentRegistry)

const stackPath = join(module.buildPath, stackFilename)

return dumpYaml(stackPath, {
provider: {
name: "faas",
gateway: getExternalGatewayUrl(provider),
},
functions: {
[module.name]: {
lang: module.spec.lang,
handler: module.spec.handler,
image,
environment: envVars,
},
},
})
}

function getExternalGatewayUrl(provider: OpenFaasProvider) {
const k8sProvider = getK8sProvider(provider.dependencies)
const hostname = provider.config.hostname
const ingressPort = k8sProvider.config.ingressHttpPort
return `http://${hostname}:${ingressPort}`
}
192 changes: 192 additions & 0 deletions garden-service/src/plugins/openfaas/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Copyright (C) 2018 Garden Technologies, Inc. <info@garden.io>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import dedent = require("dedent")
import { join } from "path"
import { resolve as urlResolve } from "url"
import { ConfigurationError } from "../../exceptions"
import { PluginContext } from "../../plugin-context"
import { joiArray, joiProviderName, joi } from "../../config/common"
import { Module } from "../../types/module"
import { Service } from "../../types/service"
import { ExecModuleSpec, execModuleSpecSchema, ExecTestSpec } from "../exec"
import { KubernetesProvider } from "../kubernetes/config"
import { CommonServiceSpec } from "../../config/service"
import { Provider, providerConfigBaseSchema, ProviderConfig } from "../../config/provider"
import { keyBy, union } from "lodash"
import { ContainerModule } from "../container/config"
import { ConfigureModuleParams, ConfigureModuleResult } from "../../types/plugin/module/configure"
import { getNamespace } from "../kubernetes/namespace"
import { LogEntry } from "../../logger/log-entry"

export interface OpenFaasModuleSpec extends ExecModuleSpec {
handler: string
image: string
lang: string
}

export const openfaasModuleSpecSchema = execModuleSpecSchema
.keys({
dependencies: joiArray(joi.string())
.description("The names of services/functions that this function depends on at runtime."),
handler: joi.string()
.default(".")
.posixPath({ subPathOnly: true })
.description("Specify which directory under the module contains the handler file/function."),
image: joi.string()
.description("The image name to use for the built OpenFaaS container (defaults to the module name)"),
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()
.keys({
endpoint: joi.string()
.uri()
.required()
.description(`The full URL to query this service _from within_ the cluster.`),
})

export interface OpenFaasModule extends Module<OpenFaasModuleSpec, CommonServiceSpec, ExecTestSpec> { }
export type OpenFaasModuleConfig = OpenFaasModule["_ConfigType"]
export interface OpenFaasService extends Service<OpenFaasModule> { }

export interface OpenFaasConfig extends ProviderConfig {
hostname: string
}

export const configSchema = providerConfigBaseSchema
.keys({
name: joiProviderName("openfaas"),
hostname: joi.string()
.hostname()
.description(dedent`
The hostname to configure for the function gateway.
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.`,
)
.example("functions.mydomain.com"),
})

export type OpenFaasProvider = Provider<OpenFaasConfig>
export type OpenFaasPluginContext = PluginContext<OpenFaasConfig>

export async function describeType() {
return {
docs: dedent`
Deploy [OpenFaaS](https://www.openfaas.com/) functions using Garden. Requires either the \`kubernetes\` or
\`local-kubernetes\` provider to be configured. Everything else is installed automatically.
`,
outputsSchema: openfaasModuleOutputsSchema,
schema: openfaasModuleSpecSchema,
}
}

export function getK8sProvider(providers: Provider[]): KubernetesProvider {
const providerMap = keyBy(providers, "name")
const provider = <KubernetesProvider>(providerMap["local-kubernetes"] || providerMap.kubernetes)

if (!provider) {
throw new ConfigurationError(`openfaas requires a kubernetes (or local-kubernetes) provider to be configured`, {
configuredProviders: Object.keys(providers),
})
}

return provider
}

export function getContainerModule(module: OpenFaasModule): ContainerModule {
const containerModule = {
...module,
spec: {
...module.spec,
buildArgs: {},
dockerfile: "Dockerfile",
services: [],
tasks: [],
tests: [],
},
}

return {
...containerModule,
buildPath: join(module.buildPath, "build", module.name),
_ConfigType: {
...containerModule,
serviceConfigs: [],
taskConfigs: [],
testConfigs: [],
},
serviceConfigs: [],
taskConfigs: [],
testConfigs: [],
}
}

export async function configureModule(
{ ctx, log, moduleConfig }: ConfigureModuleParams<OpenFaasModule>,
): Promise<ConfigureModuleResult> {
moduleConfig.build.dependencies.push({
name: "templates",
plugin: ctx.provider.name,
copy: [{
source: "template",
target: ".",
}],
})

const dependencies = [`${ctx.provider.name}--system`]

moduleConfig.serviceConfigs = [{
dependencies,
hotReloadable: false,
name: moduleConfig.name,
spec: {
name: moduleConfig.name,
dependencies,
},
}]

moduleConfig.testConfigs = moduleConfig.spec.tests.map(t => ({
name: t.name,
dependencies: union(t.dependencies, dependencies),
spec: t,
timeout: t.timeout,
}))

moduleConfig.outputs = {
endpoint: await getInternalServiceUrl(<OpenFaasPluginContext>ctx, log, moduleConfig),
}

return moduleConfig
}

async function getInternalGatewayUrl(ctx: PluginContext<OpenFaasConfig>, log: LogEntry) {
const k8sProvider = getK8sProvider(ctx.provider.dependencies)
const namespace = await getNamespace({
configStore: ctx.configStore,
log,
projectName: ctx.projectName,
provider: k8sProvider,
skipCreate: true,
})
return `http://gateway.${namespace}.svc.cluster.local:8080`
}

async function getInternalServiceUrl(ctx: PluginContext<OpenFaasConfig>, log: LogEntry, config: OpenFaasModuleConfig) {
return urlResolve(await getInternalGatewayUrl(ctx, log), getServicePath(config))
}

export function getServicePath(config: OpenFaasModuleConfig) {
return join("/", "function", config.name)
}
12 changes: 6 additions & 6 deletions garden-service/src/plugins/openfaas/faas-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.3/faas-cli-darwin",
sha256: "fe3d7933189e234fe9a395ed685584cafac9b36013c3898d1c0dc046a0bdd127",
url: "https://github.com/openfaas/faas-cli/releases/download/0.8.21/faas-cli-darwin",
sha256: "68d99f789e2e0a763b6f58f075f0118b8828fd43b3ca4eed646961eb6ac352fa",
},
linux: {
url: "https://github.com/openfaas/faas-cli/releases/download/0.8.3/faas-cli",
sha256: "d6a633248b89f4d72ee7113e33e1489e016f111472f5669ff37a01730d20445a",
url: "https://github.com/openfaas/faas-cli/releases/download/0.8.21/faas-cli",
sha256: "b8a5b455f20b14751140cb63277ee4d435e23ed041be1898a0dc2c27ee718046",
},
win32: {
url: "https://github.com/openfaas/faas-cli/releases/download/0.8.3/faas-cli.exe",
sha256: "07a191342c7cbbf3d27dbca13a3d318e6eb8941bf055eef09c6e65ba93c77d80",
url: "https://github.com/openfaas/faas-cli/releases/download/0.8.21/faas-cli.exe",
sha256: "366e01a364e64f90bec6b8234c2bc5bb87bbd059b187f8afe43c36d22f4d5b84",
},
},
})
Loading

0 comments on commit a0d913d

Please sign in to comment.