Skip to content

Commit

Permalink
fix: re-implemented local GCF plugin to fix issues
Browse files Browse the repository at this point in the history
Also gets rid of dependency on host volumes.
  • Loading branch information
edvald committed Apr 22, 2018
1 parent 212304a commit 3f2ee33
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 122 deletions.
8 changes: 7 additions & 1 deletion src/plugins/google/google-cloud-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -47,6 +52,7 @@ export const gcfServicesSchema = Joi.object()
.default(() => ({}), "{}")

export class GoogleCloudFunctionsModule extends Module<GoogleCloudFunctionsModuleConfig> { }
export class GoogleCloudFunctionsService extends Service<GoogleCloudFunctionsModule> { }

const pluginName = "google-cloud-functions"

Expand Down
198 changes: 94 additions & 104 deletions src/plugins/local/local-google-cloud-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GoogleCloudFunctionsModule>) {
moduleConfig.build.dependencies.push({
name: baseContainerName,
copy: [],
})

const module = new GoogleCloudFunctionsModule(ctx, moduleConfig)

// TODO: check that each function exists at the specified path
Expand All @@ -47,111 +60,88 @@ export const gardenPlugin = (): GardenPlugin => ({
return module
},

getServiceStatus,

async deployService(
{ ctx, provider, service, env }: DeployServiceParams<GoogleCloudFunctionsModule>,
) {
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<GoogleCloudFunctionsModule>) {
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<GoogleCloudFunctionsModule>) {
const baseModule = <ContainerModule>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<GoogleCloudFunctionsModule>,
): Promise<ServiceStatus> {
const emulator = await getEmulatorService(ctx, service)
return ctx.getServiceStatus(emulator)
},

async getServiceOutputs({ ctx, service }: GetServiceOutputsParams<GoogleCloudFunctionsModule>) {
const emulator = await getEmulatorService(ctx)
async deployService({ ctx, service }: DeployServiceParams<GoogleCloudFunctionsModule>) {
const emulatorService = await getEmulatorService(ctx, service)
return ctx.deployService(emulatorService)
},

async getServiceOutputs({ service }: GetServiceOutputsParams<GoogleCloudFunctionsModule>) {
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<GoogleCloudFunctionsModule>) {
const emulator = await getEmulatorService(ctx)
// TODO: filter to only relevant function logs
async getServiceLogs({ ctx, service, stream, tail }: GetServiceLogsParams<GoogleCloudFunctionsModule>) {
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<GoogleCloudFunctionsModule>,
): Promise<ServiceStatus> {
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: <ServicePortProtocol>"TCP", containerPort: 8010 },
},
volumes: [],
}
})

const config = await module.getConfig()
const version = await module.getVersion()

return new ContainerModule(ctx, <ContainerModuleConfig>{
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<ContainerModule>(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)
}
9 changes: 5 additions & 4 deletions static/local-gcf-container/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 []
4 changes: 4 additions & 0 deletions static/local-gcf-container/child/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ARG baseImageName
FROM ${baseImageName}

ADD . /functions
13 changes: 1 addition & 12 deletions static/local-gcf-container/garden.yml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 11 additions & 1 deletion static/local-gcf-container/start.sh
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 3f2ee33

Please sign in to comment.