@pbvision/cloud-run-service 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env ADDED
@@ -0,0 +1,9 @@
1
+ CLOUD_TASKS_EMULATOR_PORT=8123
2
+ FIRESTORE_EMULATOR_HOST=[::1]:8404
3
+ GCLOUD_PROJECT=localhost-emulator
4
+ K_REVISION=unittest
5
+ LOCAL_SERVICE_PORT_MAP='{}'
6
+ NODE_ENV=localhost
7
+ PORT=8080
8
+ REGION=us-central1
9
+ SERVICE=tbd
@@ -21,7 +21,7 @@ jobs:
21
21
  shell: bash
22
22
  - name: 'Setup Firestore Emulator'
23
23
  run: |
24
- echo '{ "projects": { "default": "fake-for-testing-only" } }' > ./.firebaserc
24
+ echo '{ "projects": { "default": "localhost-emulator" } }' > ./.firebaserc
25
25
  echo '{ "firestore": {} }' > ./firebase.json
26
26
  npm install -g firebase-tools
27
27
  firebase setup:emulators:firestore
@@ -24,10 +24,12 @@
24
24
  "standard.autoFixOnSave": true,
25
25
  "standard.usePackageJson": true,
26
26
  "cSpell.words": [
27
+ "cloudsdktool",
27
28
  "cloudtasks",
28
29
  "esbenp",
29
30
  "fastify",
30
31
  "firestore",
32
+ "gcloud",
31
33
  "gserviceaccount",
32
34
  "INDEBUGGER",
33
35
  "oidc",
package/Dockerfile CHANGED
@@ -6,8 +6,8 @@ WORKDIR /app
6
6
  COPY .npmrc package.json yarn.lock ./
7
7
  RUN yarn install --frozen-lockfile --production
8
8
 
9
- ARG PROJECT
10
- ENV PROJECT=${PROJECT}
9
+ ARG GCLOUD_PROJECT
10
+ ENV GCLOUD_PROJECT=${GCLOUD_PROJECT}
11
11
  ARG GIT_HASH
12
12
  ENV GIT_HASH=${GIT_HASH}
13
13
 
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  ## Requirements
12
12
 
13
13
  - Must run with a service account named
14
- `cr-${process.env.SERVICE}@${process.env.PROJECT}.iam.gserviceaccount.com`
14
+ `cr-${process.env.SERVICE}@${process.env.GCLOUD_PROJECT}.iam.gserviceaccount.com`
15
15
  with at least these permissions (along with whatever else your service
16
16
  requires):
17
17
  - `roles/run.invoker` - if the service needs to call other services
@@ -22,18 +22,25 @@
22
22
 
23
23
  - Environment variables:
24
24
  - Required:
25
+ - `GCLOUD_PROJECT` - the project ID this service is running in; must end in
26
+ `-dev` or `-prod`
25
27
  - `GIT_HASH` - the commit from which the current code was generated
26
28
  - `K_REVISION` - provided by cloud run (the revision ID)
27
- - Can be `localhost`
29
+ - When running a local server, it will be `localhost`
30
+ - When running it unit tests, it should be`unittest`
31
+ - If this is `unittest` then `isUnitTesting` will be true.
28
32
  - `NODE_ENV`
29
- - If this is `localhost` then `isLocalhost()` will return true.
30
- - If this is `dev` then `isDev()` will return true.
31
- - If this is `prod` then `isProd()` will return true.
32
- - `PROJECT` - the project ID this service is running in
33
- - Other endings are not allowed.
33
+ - If this is `localhost` then `isLocalhost` will be true. This is always
34
+ the case when running locally, even if we're connecting to real cloud resources (like Firestore or Task Queue).
35
+ - `isCloud` is the opposite of `isLocalhost`.
36
+ - If this is `dev` then `isDev` will be true.
37
+ - If this is `prod` then `isProd` will be true.
34
38
  - `REGION` - the region this service's cloud run is located in
35
39
  - `SERVICE` - the name of this service
36
40
  - Optional:
41
+ - Emulation (must provide all or none of these) for GCP services:
42
+ - `CLOUD_TASKS_EMULATOR_PORT`
43
+ - `FIRESTORE_EMULATOR_HOST`
37
44
  - `CLOUD_RUN_HOSTNAME_SUFFIX` - the hostname of our cloud run instances in
38
45
  this region, excluding the `SERVICE` name portion at the beginning. Only
39
46
  required if using the `callServiceAPI()` function.
@@ -10,7 +10,7 @@ steps:
10
10
  [
11
11
  "build",
12
12
  "--build-arg",
13
- "PROJECT=$PROJECT_ID",
13
+ "GCLOUD_PROJECT=$PROJECT_ID",
14
14
  "--build-arg",
15
15
  "GIT_HASH=$COMMIT_SHA",
16
16
  "--tag",
@@ -10,7 +10,7 @@ steps:
10
10
  [
11
11
  "build",
12
12
  "--build-arg",
13
- "PROJECT=$PROJECT_ID",
13
+ "GCLOUD_PROJECT=$PROJECT_ID",
14
14
  "--build-arg",
15
15
  "GIT_HASH=$COMMIT_SHA",
16
16
  "--tag",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pbvision/cloud-run-service",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "fastify-firestore-service Web Framework on Cloud Run",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,14 +10,13 @@
10
10
  "exports": "./src/index.js",
11
11
  "scripts": {
12
12
  "coverage": "yarn -s start-local-db && yarn -s test --coverage",
13
- "debug": "yarn -s start-local-db && GCLOUD_PROJECT=xyz-dev FIRESTORE_EMULATOR_HOST=[::1]:8404 INDEBUGGER=1 NODE_ENV=localhost PORT=8080 K_REVISION=unittest REGION=us-central1 PROJECT=xyz-dev GIT_HASH=`git rev-parse HEAD` SERVICE=tbd ./node_modules/nodemon/bin/nodemon.js --no-lazy --legacy-watch --watch ./src --watch ./test --inspect=9229 node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config=./jest.config.json --runInBand",
13
+ "debug": "./node_modules/nodemon/bin/nodemon.js --no-lazy --legacy-watch --watch ./src --watch ./test --inspect=9229 node --experimental-vm-modules --exec yarn test --runInBand",
14
14
  "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
15
15
  "setup": "yarn install --frozen-lockfile",
16
- "start-docker": "wd=`pwd`; tag=`basename $wd`; docker build --build-arg PROJECT=xyz-dev --build-arg GIT_HASH=`git rev-parse HEAD` . --tag $tag && docker run -p 127.0.0.1:8080:8080/tcp --env REGION='us-central1' --env PORT=8080 --env K_REVISION=localhost --env SERVICE=tbd $tag:latest && docker run $tag:latest",
17
16
  "start-local-db": "./node_modules/@pbvision/firestore-orm/scripts/start-local-db.sh",
17
+ "start-local": "yarn -s start-local-db && K_REVISION=localhost node --env-file=.env test/main.js",
18
18
  "test": "yarn -s start-local-db && yarn -s test-without-starting-db",
19
- "test-without-starting-db": "GCLOUD_PROJECT=xyz-dev FIRESTORE_EMULATOR_HOST=${FIRESTORE_EMULATOR_HOST:=[::1]:8404} NODE_ENV=localhost PORT=8080 K_REVISION='unittest' REGION=us-central1 PROJECT=xyz-dev GIT_HASH=`git rev-parse HEAD` SERVICE=tbd node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config=./jest.config.json",
20
- "start-local": "yarn -s start-local-db && FIRESTORE_EMULATOR_HOST=[::1]:8404 NODE_ENV=localhost PROJECT=xyz-dev REGION=us-central1 PORT=8080 K_REVISION=localhost PROJECT=xyz-dev GIT_HASH=`git rev-parse HEAD` node src/main.js",
19
+ "test-without-starting-db": "node --env-file=.env --experimental-vm-modules ./node_modules/jest/bin/jest.js --config=./jest.config.json",
21
20
  "watch": "yarn -s test --watch"
22
21
  },
23
22
  "repository": {
@@ -29,7 +28,7 @@
29
28
  },
30
29
  "dependencies": {
31
30
  "@google-cloud/tasks": "^4.0.1",
32
- "@pbvision/fastify-firestore-service": "^0.0.24",
31
+ "@pbvision/fastify-firestore-service": "^0.0.25",
33
32
  "google-auth-library": "^9.4.2"
34
33
  },
35
34
  "devDependencies": {
package/src/app.js CHANGED
@@ -4,7 +4,6 @@ import { port } from './port.js'
4
4
  import { isProd } from './utils.js'
5
5
 
6
6
  export async function makePBVService (components, customizePinoOpts) {
7
- const isProdEnv = isProd()
8
7
  return makeService({
9
8
  service: process.env.SERVICE,
10
9
  components,
@@ -15,16 +14,16 @@ export async function makePBVService (components, customizePinoOpts) {
15
14
  path: '/_healthcheck'
16
15
  },
17
16
  latencyTracker: {
18
- disabled: isProdEnv
17
+ disabled: isProd
19
18
  },
20
19
  logging: {
21
20
  customizePinoOpts,
22
- reportErrorDetail: !isProdEnv,
21
+ reportErrorDetail: !isProd,
23
22
  reportAllErrors: true,
24
23
  sentryDSN: process.env.sentryDSN
25
24
  },
26
25
  swagger: {
27
- disabled: isProdEnv,
26
+ disabled: isProd,
28
27
  servers: [`http://localhost:${port}`],
29
28
  routePrefix: '/app/docs'
30
29
  }
package/src/main.js CHANGED
@@ -5,20 +5,27 @@ import { API } from '@pbvision/fastify-firestore-service'
5
5
  import { makePBVService } from './app.js'
6
6
  import { callServiceAPI } from './call-service-api.js'
7
7
  import { port } from './port.js'
8
+ import { isCloud, isLocalhost } from './utils.js'
8
9
 
9
10
  API.prototype.callServiceAPI = callServiceAPI
10
11
 
11
12
  let service
12
- const project = process.env.PROJECT
13
+ const project = process.env.GCLOUD_PROJECT
13
14
 
14
15
  function verifyEnvironmentVariables () {
15
16
  const requiredEnvKeys = [
16
- 'GIT_HASH', 'K_REVISION', 'NODE_ENV', 'PROJECT', 'REGION', 'SERVICE']
17
+ 'GCLOUD_PROJECT', 'K_REVISION', 'NODE_ENV', 'REGION', 'SERVICE']
18
+ // istanbul ignore else
19
+ if (isLocalhost) {
20
+ assert(['localhost', 'unittest'].indexOf(process.env.K_REVISION) !== -1,
21
+ 'K_REVISION must be "localhost" or "unittest" in this environment')
22
+ } else {
23
+ // must have GIT_HASH to send to sentry in the cloud
24
+ requiredEnvKeys.push('GIT_HASH')
25
+ }
17
26
  for (const k of requiredEnvKeys) {
18
27
  assert(process.env[k], `${k} environment variable must be set`)
19
28
  }
20
- assert(['localhost', 'dev', 'prod'].indexOf(process.env.NODE_ENV) !== -1,
21
- `invalid NODE_ENV: ${process.env.NODE_ENV}`)
22
29
  }
23
30
 
24
31
  function makeCustomizeLoggingOptionsFunction () {
@@ -57,7 +64,7 @@ export async function makeService (components) {
57
64
 
58
65
  // istanbul ignore next
59
66
  export async function runService (components) {
60
- if (process.env.K_REVISION !== 'unittest') {
67
+ if (isCloud) {
61
68
  // if the instance tells us it will shutdown, try to shut down gracefully (for
62
69
  // example flushing logs)
63
70
  process.on('SIGTERM', () => {
@@ -75,8 +82,8 @@ export async function runService (components) {
75
82
  console.log('an error happened', err)
76
83
  }), 7000)
77
84
  })
78
- // start the server
79
- service = await makeService(components)
80
- service.listen({ port, host: '0.0.0.0' })
81
85
  }
86
+ // start the server
87
+ service = await makeService(components)
88
+ service.listen({ port, host: '0.0.0.0' })
82
89
  }
@@ -35,7 +35,7 @@ export class TestCallServiceAPI extends API {
35
35
  static RESPONSE = { code: S.int, body: S.str }
36
36
 
37
37
  async computeResponse () {
38
- assert(isUnitTesting())
38
+ assert(isUnitTesting)
39
39
  const resp = await this.callServiceAPI(this.req.body)
40
40
  return {
41
41
  code: resp.code,
package/src/tasks.js CHANGED
@@ -1,8 +1,19 @@
1
1
  import { CloudTasksClient } from '@google-cloud/tasks'
2
+ import { credentials } from '@grpc/grpc-js'
2
3
 
3
- import { getServiceProtocolAndHost } from './utils.js'
4
+ import { getServiceProtocolAndHost, usingEmulator } from './utils.js'
4
5
 
5
- const tasksClient = new CloudTasksClient()
6
+ const tasksClient = (() => {
7
+ if (usingEmulator) {
8
+ return new CloudTasksClient({
9
+ port: process.env.CLOUD_TASKS_EMULATOR_PORT,
10
+ servicePath: 'localhost',
11
+ sslCreds: credentials.createInsecure()
12
+ })
13
+ }
14
+ // istanbul ignore next
15
+ return new CloudTasksClient()
16
+ })()
6
17
 
7
18
  /**
8
19
  * Add a Task to a Cloud Tasks queue.
@@ -28,8 +39,9 @@ export async function enqueueCloudTask ({
28
39
  service = 'internal',
29
40
  name = undefined, ignoreNameAlreadyUsedError = false
30
41
  }) {
31
- const parent = tasksClient.queuePath(
32
- process.env.PROJECT, process.env.REGION, queue)
42
+ const project = process.env.GCLOUD_PROJECT
43
+ const region = process.env.REGION
44
+ const parent = tasksClient.queuePath(project, region, queue)
33
45
  const protocolAndHost = getServiceProtocolAndHost(service)
34
46
  const task = {
35
47
  httpRequest: {
@@ -40,12 +52,12 @@ export async function enqueueCloudTask ({
40
52
  url: `${protocolAndHost}/${queue.replace(/-/g, '_')}`,
41
53
  body: Buffer.from(JSON.stringify(payload)).toString('base64'),
42
54
  oidcToken: {
43
- serviceAccountEmail: `cr-${process.env.SERVICE}@${process.env.PROJECT}.iam.gserviceaccount.com`
55
+ serviceAccountEmail: `cr-${process.env.SERVICE}@${project}.iam.gserviceaccount.com`
44
56
  }
45
57
  }
46
58
  }
47
59
  if (name) {
48
- const fqName = tasksClient.taskPath(process.env.PROJECT, process.env.REGION, queue, name)
60
+ const fqName = tasksClient.taskPath(project, region, queue, name)
49
61
  task.name = fqName
50
62
  }
51
63
  const request = { parent, task }
package/src/utils.js CHANGED
@@ -2,6 +2,25 @@ import assert from 'node:assert'
2
2
 
3
3
  import { port as portForThisService } from './port.js'
4
4
 
5
+ // this refers to where the code is running; the database and other services
6
+ // we are connected to depends on GCLOUD_PROJECT
7
+ export const isLocalhost = process.env.NODE_ENV === 'localhost'
8
+ export const isCloud = !isLocalhost
9
+ export const isDev = process.env.NODE_ENV === 'dev'
10
+ export const isProd = process.env.NODE_ENV === 'prod'
11
+ // istanbul ignore next
12
+ assert(isLocalhost || isDev || isProd,
13
+ 'invalid NODE_ENV: must be on localhost, dev or prod')
14
+
15
+ export const isUnitTesting = process.env.K_REVISION === 'unittest'
16
+ assert(!isUnitTesting || isLocalhost, 'must be on localhost if unit testing')
17
+
18
+ export const usingEmulator = !!process.env.FIRESTORE_EMULATOR_HOST
19
+ assert(!usingEmulator || process.env.CLOUD_TASKS_EMULATOR_PORT,
20
+ 'both firestore and tasks need to be either emulated or not')
21
+ assert(usingEmulator === (process.env.GCLOUD_PROJECT === 'localhost-emulator'),
22
+ 'when using the emulator, GCLOUD_PROJECT should be set to localhost-emulator')
23
+
5
24
  export function getServiceProtocolAndHost (serviceName) {
6
25
  const host = getServiceHost(serviceName)
7
26
  // istanbul ignore next
@@ -10,11 +29,12 @@ export function getServiceProtocolAndHost (serviceName) {
10
29
  }
11
30
 
12
31
  export function getServiceHost (serviceName) {
13
- if (isLocalhost()) {
32
+ // manually check NODE_ENV for testing purposes
33
+ if (process.env.NODE_ENV === 'localhost') {
14
34
  if (process.env.SERVICE === serviceName) {
15
35
  return `localhost:${portForThisService}`
16
36
  }
17
- const portMapping = JSON.parse(process.env.LOCAL_SERVICE_PORT_MAP ?? 'null')
37
+ const portMapping = JSON.parse(process.env.LOCAL_SERVICE_PORT_MAP)
18
38
  const port = portMapping[serviceName]
19
39
  assert(port, `unknown service or missing port for localhost ${serviceName}`)
20
40
  return `localhost:${port}`
@@ -23,25 +43,4 @@ export function getServiceHost (serviceName) {
23
43
  }
24
44
  }
25
45
 
26
- export function isLocalhost () {
27
- assert(process.env.NODE_ENV)
28
- return process.env.NODE_ENV === 'localhost'
29
- }
30
-
31
- export function isUnitTesting () {
32
- const ret = process.env.K_REVISION === 'unittest'
33
- assert(!ret || isLocalhost(), 'must on localhost if in unit tests')
34
- return ret
35
- }
36
-
37
- export function isProd () {
38
- assert(process.env.NODE_ENV)
39
- return process.env.NODE_ENV === 'prod'
40
- }
41
-
42
- export function isDev () {
43
- assert(process.env.NODE_ENV)
44
- return process.env.NODE_ENV === 'dev'
45
- }
46
-
47
46
  export const now = () => Math.floor(new Date().getTime() / 1000)
package/test/main.js ADDED
@@ -0,0 +1,4 @@
1
+ import { runService } from '../src/index.js'
2
+ const { TestAPI, TestCallServiceAPI } = await import('../src/placeholder.js')
3
+
4
+ await runService({ TestAPI, TestCallServiceAPI })
@@ -38,16 +38,16 @@ class TestTasks extends BaseTest {
38
38
  await promise
39
39
  }
40
40
  expect(CloudTasksClient.prototype.createTask).toHaveBeenCalledWith({
41
- // assuming values for project (xyz-dev) and region (us-central1) and
41
+ // assuming values for project (localhost-emulator) and region (us-central1) and
42
42
  // service (tbd)
43
- parent: 'projects/xyz-dev/locations/us-central1/queues/test-queue',
43
+ parent: 'projects/localhost-emulator/locations/us-central1/queues/test-queue',
44
44
  task: {
45
45
  httpRequest: {
46
46
  body: Buffer.from(JSON.stringify(args.payload)).toString('base64'),
47
47
  headers: { 'Content-Type': 'application/json' },
48
48
  httpMethod: 'POST',
49
49
  oidcToken: {
50
- serviceAccountEmail: 'cr-tbd@xyz-dev.iam.gserviceaccount.com'
50
+ serviceAccountEmail: 'cr-tbd@localhost-emulator.iam.gserviceaccount.com'
51
51
  },
52
52
  url: 'http://localhost:8888/test_queue'
53
53
  },
@@ -63,7 +63,7 @@ class TestTasks extends BaseTest {
63
63
  async testEnqueueTaskWithName () {
64
64
  await this.check(
65
65
  { name: 'x', payload: { x: 3 } },
66
- { name: 'projects/xyz-dev/locations/us-central1/queues/test-queue/tasks/x' })
66
+ { name: 'projects/localhost-emulator/locations/us-central1/queues/test-queue/tasks/x' })
67
67
  }
68
68
 
69
69
  async testEnqueueTaskWithNameThatWasRecentlyUsedNOTOkay () {
@@ -74,7 +74,7 @@ class TestTasks extends BaseTest {
74
74
  })
75
75
  await this.check(
76
76
  { name: 'x', payload: { x: 3 }, ignoreNameAlreadyUsedError: true },
77
- { name: 'projects/xyz-dev/locations/us-central1/queues/test-queue/tasks/x' })
77
+ { name: 'projects/localhost-emulator/locations/us-central1/queues/test-queue/tasks/x' })
78
78
  }
79
79
 
80
80
  async testEnqueueTaskWithNameThatWasRecentlyUsedAndThatsOkay () {
@@ -85,7 +85,7 @@ class TestTasks extends BaseTest {
85
85
  })
86
86
  await this.check(
87
87
  { name: 'x', payload: { x: 3 } },
88
- { name: 'projects/xyz-dev/locations/us-central1/queues/test-queue/tasks/x' },
88
+ { name: 'projects/localhost-emulator/locations/us-central1/queues/test-queue/tasks/x' },
89
89
  'task name already used recently: x')
90
90
  }
91
91
 
@@ -96,7 +96,7 @@ class TestTasks extends BaseTest {
96
96
  })
97
97
  await this.check(
98
98
  { name: 'x', payload: { x: 3 } },
99
- { name: 'projects/xyz-dev/locations/us-central1/queues/test-queue/tasks/x' },
99
+ { name: 'projects/localhost-emulator/locations/us-central1/queues/test-queue/tasks/x' },
100
100
  'not in right format')
101
101
  }
102
102
  }
@@ -2,7 +2,7 @@ import { jest } from '@jest/globals'
2
2
 
3
3
  import { BaseTest, runTests } from '../node_modules/@pbvision/fastify-firestore-service/test/base-test.js'
4
4
  import { port } from '../src/port.js'
5
- import { getServiceHost, isDev, isLocalhost, isProd } from '../src/utils.js'
5
+ import { getServiceHost, isCloud, isDev, isLocalhost, isProd, usingEmulator } from '../src/utils.js'
6
6
 
7
7
  const ORIG_ENV = process.env
8
8
 
@@ -17,21 +17,14 @@ class TestUtils extends BaseTest {
17
17
  }
18
18
 
19
19
  testEnv () {
20
+ expect(process.env.GIT_HASH).toBe(undefined)
20
21
  expect(process.env.K_REVISION).toBe('unittest')
21
22
  expect(process.env.NODE_ENV).toBe('localhost')
22
- expect(isLocalhost()).toBe(true)
23
- expect(isProd()).toBe(false)
24
- expect(isDev()).toBe(false)
25
-
26
- process.env.NODE_ENV = 'dev'
27
- expect(isLocalhost()).toBe(false)
28
- expect(isProd()).toBe(false)
29
- expect(isDev()).toBe(true)
30
-
31
- process.env.NODE_ENV = 'prod'
32
- expect(isLocalhost()).toBe(false)
33
- expect(isProd()).toBe(true)
34
- expect(isDev()).toBe(false)
23
+ expect(isLocalhost).toBe(true)
24
+ expect(isCloud).toBe(false)
25
+ expect(isProd).toBe(false)
26
+ expect(isDev).toBe(false)
27
+ expect(usingEmulator).toBe(true)
35
28
  }
36
29
 
37
30
  testGetServiceHost () {