diff --git a/src/plugins/google/google-cloud-functions.ts b/src/plugins/google/google-cloud-functions.ts index 89d2d3d503..43dfe97e23 100644 --- a/src/plugins/google/google-cloud-functions.ts +++ b/src/plugins/google/google-cloud-functions.ts @@ -8,7 +8,12 @@ import { identifierRegex, validate } from "../../types/common" import { baseServiceSchema, Module, ModuleConfig } from "../../types/module" -import { ServiceConfig, ServiceState, ServiceStatus } from "../../types/service" +import { + ServiceConfig, + ServiceState, + ServiceStatus, + Service, +} from "../../types/service" import { resolve, } from "path" @@ -47,6 +52,7 @@ export const gcfServicesSchema = Joi.object() .default(() => ({}), "{}") export class GoogleCloudFunctionsModule extends Module { } +export class GoogleCloudFunctionsService extends Service { } const pluginName = "google-cloud-functions" diff --git a/src/plugins/local/local-google-cloud-functions.ts b/src/plugins/local/local-google-cloud-functions.ts index 2027d27ffd..cdd8705d24 100644 --- a/src/plugins/local/local-google-cloud-functions.ts +++ b/src/plugins/local/local-google-cloud-functions.ts @@ -8,34 +8,47 @@ import { PluginContext } from "../../plugin-context" import { ServiceStatus } from "../../types/service" -import { join, relative, resolve } from "path" -import * as escapeStringRegexp from "escape-string-regexp" -import { DeploymentError, PluginError } from "../../exceptions" +import { join } from "path" import { - gcfServicesSchema, GoogleCloudFunctionsModule, + gcfServicesSchema, + GoogleCloudFunctionsModule, + GoogleCloudFunctionsService, } from "../google/google-cloud-functions" import { - ConfigureEnvironmentParams, - DeployServiceParams, GetEnvironmentStatusParams, GetServiceLogsParams, GetServiceOutputsParams, - GetServiceStatusParams, ParseModuleParams, + DeployServiceParams, + GetServiceLogsParams, + GetServiceOutputsParams, + GetServiceStatusParams, + ParseModuleParams, GardenPlugin, + BuildModuleParams, + GetModuleBuildStatusParams, } from "../../types/plugin" import { STATIC_DIR } from "../../constants" -import { ContainerModule, ContainerService } from "../container" +import { + ContainerModule, + ContainerModuleConfig, + ContainerService, + ServicePortProtocol, +} from "../container" import { validate } from "../../types/common" +import { mapValues } from "lodash" -const emulatorModulePath = join(STATIC_DIR, "local-gcf-container") +const baseContainerName = "local-google-cloud-functions.local-gcf-container" +const emulatorBaseModulePath = join(STATIC_DIR, "local-gcf-container") const emulatorPort = 8010 -const emulatorServiceName = "google-cloud-functions" export const gardenPlugin = (): GardenPlugin => ({ - actions: { - getEnvironmentStatus, - configureEnvironment, - }, + modules: [emulatorBaseModulePath], + moduleActions: { "google-cloud-function": { async parseModule({ ctx, moduleConfig }: ParseModuleParams) { + moduleConfig.build.dependencies.push({ + name: baseContainerName, + copy: [], + }) + const module = new GoogleCloudFunctionsModule(ctx, moduleConfig) // TODO: check that each function exists at the specified path @@ -47,111 +60,88 @@ export const gardenPlugin = (): GardenPlugin => ({ return module }, - getServiceStatus, - - async deployService( - { ctx, provider, service, env }: DeployServiceParams, - ) { - const containerFunctionPath = resolve( - "/functions", - relative(ctx.projectRoot, service.module.path), - service.config.path, - ) - - const emulator = await getEmulatorService(ctx) - const result = await ctx.execInService( - emulator, - [ - "functions-emulator", "deploy", - "--trigger-http", - "--project", "local", - "--region", "local", - "--local-path", containerFunctionPath, - "--entry-point", service.config.entrypoint || service.name, - service.config.function, - ], - ) + async getModuleBuildStatus({ ctx, module }: GetModuleBuildStatusParams) { + const emulator = await getEmulatorModule(ctx, module) + return ctx.getModuleBuildStatus(emulator) + }, - if (result.code !== 0) { - throw new DeploymentError(`Deploying function ${service.name} failed: ${result.output}`, { - serviceName: service.name, - error: result.stderr, - }) - } + async buildModule({ ctx, module, logEntry }: BuildModuleParams) { + const baseModule = await ctx.getModule(baseContainerName) + const emulator = await getEmulatorModule(ctx, module) + const baseImageName = (await baseModule.getLocalImageId())! + return ctx.buildModule(emulator, { baseImageName }, logEntry) + }, - return getServiceStatus({ ctx, provider, service, env }) + async getServiceStatus( + { ctx, service }: GetServiceStatusParams, + ): Promise { + const emulator = await getEmulatorService(ctx, service) + return ctx.getServiceStatus(emulator) }, - async getServiceOutputs({ ctx, service }: GetServiceOutputsParams) { - const emulator = await getEmulatorService(ctx) + async deployService({ ctx, service }: DeployServiceParams) { + const emulatorService = await getEmulatorService(ctx, service) + return ctx.deployService(emulatorService) + }, + async getServiceOutputs({ service }: GetServiceOutputsParams) { return { - endpoint: `http://${emulator.name}:${emulatorPort}/local/local/${service.config.function}`, + endpoint: `http://${service.name}:${emulatorPort}/local/local/${service.config.entrypoint || service.name}`, } }, - async getServiceLogs({ ctx, stream, tail }: GetServiceLogsParams) { - const emulator = await getEmulatorService(ctx) - // TODO: filter to only relevant function logs + async getServiceLogs({ ctx, service, stream, tail }: GetServiceLogsParams) { + const emulator = await getEmulatorService(ctx, service) return ctx.getServiceLogs(emulator, stream, tail) }, }, }, }) -async function getEnvironmentStatus({ ctx }: GetEnvironmentStatusParams) { - // Check if functions emulator container is running - const status = await ctx.getServiceStatus(await getEmulatorService(ctx)) - - return { configured: status.state === "ready" } -} - -async function configureEnvironment({ ctx, logEntry }: ConfigureEnvironmentParams) { - const service = await getEmulatorService(ctx) - - // We mount the project root into the container, so we can exec deploy any function in there later. - service.config.volumes = [{ - name: "functions", - containerPath: "/functions", - hostPath: ctx.projectRoot, - }] - - // TODO: Publish this container separately from the project instead of building it here - await ctx.buildModule(service.module) - await ctx.deployService(service, undefined, logEntry) -} - -async function getServiceStatus( - { ctx, service }: GetServiceStatusParams, -): Promise { - const emulator = await getEmulatorService(ctx) - const emulatorStatus = await ctx.getServiceStatus(emulator) - - if (emulatorStatus !== "ready") { - return { state: "stopped" } - } - - const result = await ctx.execInService(emulator, ["functions-emulator", "list"]) - - // Regex fun. Yay. - // TODO: Submit issue/PR to @google-cloud/functions-emulator to get machine-readable output - if (result.output.match(new RegExp(`READY\\s+│\\s+${escapeStringRegexp(service.name)}\\s+│`, "g"))) { - // For now we don't have a way to track which version is developed. - // We most likely need to keep track of that on our side. - return { state: "ready" } - } else { - return {} - } +async function getEmulatorModule(ctx: PluginContext, module: GoogleCloudFunctionsModule) { + const services = mapValues(module.services, (s, name) => { + const functionEntrypoint = s.entrypoint || name + + return { + command: ["/app/start.sh", functionEntrypoint], + daemon: false, + dependencies: s.dependencies, + endpoints: [{ + port: "http", + }], + healthCheck: { tcpPort: "http" }, + ports: { + http: { protocol: "TCP", containerPort: 8010 }, + }, + volumes: [], + } + }) + + const config = await module.getConfig() + const version = await module.getVersion() + + return new ContainerModule(ctx, { + allowPush: true, + build: { + dependencies: config.build.dependencies.concat([{ + name: baseContainerName, + copy: [{ + source: "child/Dockerfile", + target: "Dockerfile", + }], + }]), + }, + image: `${module.name}:${version.versionString}`, + name: module.name, + path: module.path, + services, + test: config.test, + type: "container", + variables: config.variables, + }) } -async function getEmulatorService(ctx: PluginContext) { - const module = await ctx.resolveModule(emulatorModulePath) - - if (!module) { - throw new PluginError(`Could not find Google Cloud Function emulator module`, { - emulatorModulePath, - }) - } - - return ContainerService.factory(ctx, module, emulatorServiceName) +async function getEmulatorService(ctx: PluginContext, service: GoogleCloudFunctionsService) { + const emulatorModule = await getEmulatorModule(ctx, service.module) + return ContainerService.factory(ctx, emulatorModule, service.name) } diff --git a/static/local-gcf-container/Dockerfile b/static/local-gcf-container/Dockerfile index 495d459f95..0260b441ce 100644 --- a/static/local-gcf-container/Dockerfile +++ b/static/local-gcf-container/Dockerfile @@ -1,17 +1,18 @@ FROM node:6 -RUN npm install -g @google-cloud/functions-emulator +RUN npm install -g @google-cloud/functions-emulator@1.0.0-beta.4 RUN mkdir /app WORKDIR /app -RUN wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-185.0.0-linux-x86_64.tar.gz \ - && tar -zxvf google-cloud-sdk-185.0.0-linux-x86_64.tar.gz \ +RUN wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-198.0.0-linux-x86_64.tar.gz \ + && tar -zxvf google-cloud-sdk-198.0.0-linux-x86_64.tar.gz \ && ./google-cloud-sdk/install.sh \ && /app/google-cloud-sdk/bin/gcloud components install alpha beta gsutil \ - && rm -f google-cloud-sdk-185.0.0-linux-x86_64.tar.gz + && rm -f google-cloud-sdk-198.0.0-linux-x86_64.tar.gz ADD config.json /root/.config/configstore/@google-cloud/functions-emulator/config.json ADD start.sh /app/start.sh ENTRYPOINT ["/app/start.sh"] +CMD [] diff --git a/static/local-gcf-container/child/Dockerfile b/static/local-gcf-container/child/Dockerfile new file mode 100644 index 0000000000..f0988fcea6 --- /dev/null +++ b/static/local-gcf-container/child/Dockerfile @@ -0,0 +1,4 @@ +ARG baseImageName +FROM ${baseImageName} + +ADD . /functions diff --git a/static/local-gcf-container/garden.yml b/static/local-gcf-container/garden.yml index ca79f1c349..aec3e9bb42 100644 --- a/static/local-gcf-container/garden.yml +++ b/static/local-gcf-container/garden.yml @@ -1,14 +1,3 @@ module: - description: Container for running Google Cloud Functions emulator + description: Base container for running Google Cloud Functions emulator type: container - services: - google-cloud-functions: - endpoints: - - paths: [/] - port: http - ports: - http: - protocol: TCP - containerPort: 8010 - healthCheck: - tcpPort: http diff --git a/static/local-gcf-container/start.sh b/static/local-gcf-container/start.sh index 1771abfd91..4ba7a1ba17 100755 --- a/static/local-gcf-container/start.sh +++ b/static/local-gcf-container/start.sh @@ -1,3 +1,13 @@ #!/bin/sh -functions-emulator start --bindHost 0.0.0.0 --tail "$@" +cd /functions + +functions-emulator start --bindHost 0.0.0.0 + +functions-emulator deploy $2 \ + --trigger-http \ + --project local \ + --region local + +functions-emulator stop > /dev/null +functions-emulator start --bindHost 0.0.0.0 --tail