Skip to content

Commit 681c62f

Browse files
authored
feat: support workers in docker SOFIE-4071 (#236)
1 parent 6d62e95 commit 681c62f

11 files changed

Lines changed: 207 additions & 21 deletions

File tree

.editorconfig

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@
22
indent_style = tab
33

44
[*.{cs,js,ts,json}]
5-
indent_size = 4
5+
indent_size = 4
6+
7+
[*.{yml,yaml}]
8+
indent_size = 2
9+
tab_width = 2
10+
indent_style = space

.github/workflows/publish-prerelease-docker.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262

6363
strategy:
6464
matrix:
65-
repo: [package-manager, workforce, http-server, quantel-http-transformer-proxy]
65+
repo: [package-manager, workforce, http-server, quantel-http-transformer-proxy, worker]
6666

6767
steps:
6868
- uses: actions/checkout@v5

apps/appcontainer-node/packages/generic/src/appContainer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,12 @@ export class AppContainer {
101101
throw new Error(`Unknown app "${clientId}" just connected to the appContainer`)
102102
}
103103
client.once('close', () => {
104-
this.logger.warn(`Connection to Worker "${clientId}" closed`)
104+
this.logger.warn(`Connection from Worker "${clientId}" closed`)
105105
app.workerAgentApi = null
106106

107107
this.workerStorage.releaseLockForTag(unprotectString(clientId))
108108
})
109-
this.logger.info(`Connection to Worker "${client.clientId}" established`)
109+
this.logger.info(`Connection from Worker "${client.clientId}" established`)
110110
app.workerAgentApi = api
111111

112112
// Set upp the app for pinging and automatic spin-down:

apps/worker/app/Dockerfile

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
FROM node:18-alpine as builder
2+
3+
# Note: Build this from the root directory:
4+
# cd package-manager
5+
# docker build -f apps/worker/app/Dockerfile -t pm-worker .
6+
# docker build -t pm-worker ../../../..
7+
8+
# Environment
9+
10+
WORKDIR /src
11+
12+
# Common
13+
14+
COPY package.json tsconfig.json tsconfig.build.json yarn.lock lerna.json commonPackage.json .yarnrc.yml ./
15+
COPY scripts ./scripts
16+
COPY .yarn ./.yarn
17+
18+
# Shared dependencies
19+
COPY shared ./shared
20+
21+
22+
# App dependencies
23+
RUN mkdir -p apps/worker
24+
COPY apps/worker/packages apps/worker/packages
25+
26+
# App
27+
COPY apps/worker/app apps/worker/app
28+
29+
# Install
30+
RUN yarn install
31+
32+
# Build
33+
RUN yarn build
34+
35+
# Purge dev-dependencies:
36+
RUN yarn workspaces focus -A --production
37+
38+
RUN rm -r scripts
39+
40+
41+
# Create deploy-image:
42+
FROM node:18-alpine
43+
44+
COPY --from=builder /src /src
45+
46+
RUN apk add --no-cache dumb-init ffmpeg
47+
48+
# Run as non-root user
49+
USER 1000
50+
WORKDIR /src/apps/worker/app
51+
# ENV APP_CONTAINER_PORT=8090
52+
# # ENV HTTP_SERVER_API_KEY_READ=
53+
# # ENV HTTP_SERVER_API_KEY_WRITE=
54+
# ENV HTTP_SERVER_BASE_PATH="/data"
55+
# EXPOSE 8090
56+
57+
ENTRYPOINT ["/usr/bin/dumb-init", "--", "./docker-entrypoint.sh"]
58+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
set -e
3+
4+
HOSTNAME=$(hostname)
5+
6+
# Inject a unique worker ID based on the hostname, and disable the appContainer connection
7+
node dist/index.js --workerId=$HOSTNAME --appContainerURL="" $*

shared/packages/api/src/config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ const workforceArguments = defineArguments({
2929
default: parseInt(process.env.WORKFORCE_PORT || '', 10) || 8070,
3030
describe: 'The port number to start the Workforce websocket server on',
3131
},
32+
allowNoAppContainers: {
33+
type: 'boolean',
34+
default: process.env.WORKFORCE_ALLOW_NO_APP_CONTAINERS === '1' || false,
35+
describe: 'If true, the workforce will not check if it has no appContainers connected',
36+
},
3237
})
3338
/** CLI-argument-definitions for the HTTP-Server process */
3439
const httpServerArguments = defineArguments({
@@ -227,7 +232,7 @@ const appContainerArguments = defineArguments({
227232
// These are passed-through to the spun-up workers:
228233
resourceId: {
229234
type: 'string',
230-
default: process.env.WORKER_NETWORK_ID || 'default',
235+
default: process.env.WORKER_RESOURCE_ID || 'default',
231236
describe: 'Identifier of the local resource/computer this worker runs on',
232237
},
233238
networkIds: {
@@ -334,6 +339,7 @@ export interface WorkforceConfig {
334339
process: ProcessConfig
335340
workforce: {
336341
port: number | null
342+
allowNoAppContainers: boolean
337343
}
338344
}
339345

@@ -349,6 +355,7 @@ export async function getWorkforceConfig(): Promise<WorkforceConfig> {
349355
process: getProcessConfig(argv),
350356
workforce: {
351357
port: argv.port,
358+
allowNoAppContainers: argv.allowNoAppContainers,
352359
},
353360
}
354361
}

shared/packages/expectationManager/src/internalManager/lib/expectationManagerServer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ export class ExpectationManagerServer {
5050
})
5151
this.manager.workerAgents.upsert(clientId, { api, connected: true })
5252
client.once('close', () => {
53-
this.logger.warn(`Connection to Worker "${clientId}" closed`)
53+
this.logger.warn(`Connection from Worker "${clientId}" closed`)
5454

5555
const workerAgent = this.manager.workerAgents.get(clientId)
5656
if (workerAgent) {
5757
workerAgent.connected = false
5858
this.manager.workerAgents.remove(clientId)
5959
}
6060
})
61-
this.logger.info(`Connection to Worker "${clientId}" established`)
61+
this.logger.info(`Connection from Worker "${clientId}" established`)
6262
this.manager.tracker.triggerEvaluationNow()
6363
break
6464
}

shared/packages/worker/src/worker/accessorHandlers/accessor.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ export function getAccessorStaticHandle(accessor: AccessorOnPackage.Any) {
4646
} else if (accessor.type === Accessor.AccessType.HTTP_PROXY) {
4747
return HTTPProxyAccessorHandle
4848
} else if (accessor.type === Accessor.AccessType.FILE_SHARE) {
49-
if (process.platform !== 'win32') throw new Error(`FileShareAccessor: not supported on ${process.platform}`)
5049
return FileShareAccessorHandle
5150
} else if (accessor.type === Accessor.AccessType.QUANTEL) {
5251
return QuantelAccessorHandle

shared/packages/worker/src/worker/accessorHandlers/fileShare.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
MonitorId,
3232
betterPathResolve,
3333
betterPathIsAbsolute,
34+
isRunningInTest,
3435
} from '@sofie-package-manager/api'
3536
import { BaseWorker } from '../worker'
3637
import { GenericWorker } from '../workers/genericWorker/genericWorker'
@@ -111,12 +112,25 @@ export class FileShareAccessorHandle<Metadata> extends GenericFileAccessorHandle
111112
return this.getFullPath(this.filePath)
112113
}
113114
static doYouSupportAccess(worker: BaseWorker, accessor: AccessorOnPackage.Any): boolean {
115+
if (!isFileShareSupportedOnCurrentPlatform()) return false
116+
114117
return defaultDoYouSupportAccess(worker, accessor)
115118
}
116119
get packageName(): string {
117120
return this.fullPath
118121
}
119122
checkHandleBasic(): AccessorHandlerCheckHandleBasicResult {
123+
if (!isFileShareSupportedOnCurrentPlatform()) {
124+
return {
125+
success: false,
126+
knownReason: true,
127+
reason: {
128+
user: `File share is not supported on this worker`,
129+
tech: `File share is not supported on ${process.platform}`,
130+
},
131+
}
132+
}
133+
120134
if (this.accessor.type !== Accessor.AccessType.FILE_SHARE) {
121135
return {
122136
success: false,
@@ -168,14 +182,46 @@ export class FileShareAccessorHandle<Metadata> extends GenericFileAccessorHandle
168182
return { success: true }
169183
}
170184
checkCompatibilityWithAccessor(): AccessorHandlerCheckHandleCompatibilityResult {
185+
if (!isFileShareSupportedOnCurrentPlatform()) {
186+
return {
187+
success: false,
188+
knownReason: true,
189+
reason: {
190+
user: `File share is not supported on this worker`,
191+
tech: `File share is not supported on ${process.platform}`,
192+
},
193+
}
194+
}
171195
return { success: true } // no special compatibility checks
172196
}
173197
checkHandleRead(): AccessorHandlerCheckHandleReadResult {
198+
if (!isFileShareSupportedOnCurrentPlatform()) {
199+
return {
200+
success: false,
201+
knownReason: true,
202+
reason: {
203+
user: `File share is not supported on this worker`,
204+
tech: `File share is not supported on ${process.platform}`,
205+
},
206+
}
207+
}
208+
174209
const defaultResult = defaultCheckHandleRead(this.accessor)
175210
if (defaultResult) return defaultResult
176211
return { success: true }
177212
}
178213
checkHandleWrite(): AccessorHandlerCheckHandleWriteResult {
214+
if (!isFileShareSupportedOnCurrentPlatform()) {
215+
return {
216+
success: false,
217+
knownReason: true,
218+
reason: {
219+
user: `File share is not supported on this worker`,
220+
tech: `File share is not supported on ${process.platform}`,
221+
},
222+
}
223+
}
224+
179225
const defaultResult = defaultCheckHandleWrite(this.accessor)
180226
if (defaultResult) return defaultResult
181227
return { success: true }
@@ -199,6 +245,18 @@ export class FileShareAccessorHandle<Metadata> extends GenericFileAccessorHandle
199245
return { success: true }
200246
}
201247
async tryPackageRead(): Promise<AccessorHandlerTryPackageReadResult> {
248+
if (!isFileShareSupportedOnCurrentPlatform()) {
249+
return {
250+
success: false,
251+
knownReason: true,
252+
reason: {
253+
user: `File share is not supported on this worker`,
254+
tech: `File share is not supported on ${process.platform}`,
255+
},
256+
packageExists: false,
257+
}
258+
}
259+
202260
try {
203261
// Check if we can open the file for reading:
204262
const fd = await fsOpen(this.fullPath, 'r')
@@ -232,6 +290,17 @@ export class FileShareAccessorHandle<Metadata> extends GenericFileAccessorHandle
232290
return { success: true }
233291
}
234292
private async _checkPackageReadAccess(): Promise<AccessorHandlerCheckPackageReadAccessResult> {
293+
if (!isFileShareSupportedOnCurrentPlatform()) {
294+
return {
295+
success: false,
296+
knownReason: true,
297+
reason: {
298+
user: `File share is not supported on this worker`,
299+
tech: `File share is not supported on ${process.platform}`,
300+
},
301+
}
302+
}
303+
235304
await this.prepareFileAccess()
236305

237306
try {
@@ -364,6 +433,17 @@ export class FileShareAccessorHandle<Metadata> extends GenericFileAccessorHandle
364433
await this.unlinkIfExists(this.metadataPath)
365434
}
366435
async runCronJob(packageContainerExp: PackageContainerExpectation): Promise<AccessorHandlerRunCronJobResult> {
436+
if (!isFileShareSupportedOnCurrentPlatform()) {
437+
return {
438+
success: false,
439+
knownReason: true,
440+
reason: {
441+
user: `File share is not supported on this worker`,
442+
tech: `File share is not supported on ${process.platform}`,
443+
},
444+
}
445+
}
446+
367447
// Always check read/write access first:
368448
const checkRead = await this.checkPackageContainerReadAccess()
369449
if (!checkRead.success) return checkRead
@@ -396,6 +476,17 @@ export class FileShareAccessorHandle<Metadata> extends GenericFileAccessorHandle
396476
async setupPackageContainerMonitors(
397477
packageContainerExp: PackageContainerExpectation
398478
): Promise<SetupPackageContainerMonitorsResult> {
479+
if (!isFileShareSupportedOnCurrentPlatform()) {
480+
return {
481+
success: false,
482+
knownReason: true,
483+
reason: {
484+
user: `File share is not supported on this worker`,
485+
tech: `File share is not supported on ${process.platform}`,
486+
},
487+
}
488+
}
489+
399490
const resultingMonitors: Record<MonitorId, MonitorInProgress> = {}
400491
const monitorIds = Object.keys(
401492
packageContainerExp.monitors
@@ -419,6 +510,9 @@ export class FileShareAccessorHandle<Metadata> extends GenericFileAccessorHandle
419510
operationName: string,
420511
source: string | GenericAccessorHandle<any>
421512
): Promise<PackageOperation> {
513+
if (!isFileShareSupportedOnCurrentPlatform())
514+
throw new Error(`FileShareAccessor: not supported on ${process.platform}`)
515+
422516
await this.fileHandler.clearPackageRemoval(this.filePath)
423517
return this.logWorkOperation(operationName, source, this.packageName)
424518
}
@@ -443,6 +537,9 @@ export class FileShareAccessorHandle<Metadata> extends GenericFileAccessorHandle
443537
* This method should be called prior to any file access being made.
444538
*/
445539
async prepareFileAccess(forceRemount = false): Promise<void> {
540+
if (!isFileShareSupportedOnCurrentPlatform())
541+
throw new Error(`FileShareAccessor: not supported on ${process.platform}`)
542+
446543
if (!this.originalFolderPath) throw new Error(`FileShareAccessor: accessor.folderPath not set!`)
447544
const folderPath = this.originalFolderPath
448545

@@ -680,3 +777,8 @@ export class FileShareAccessorHandle<Metadata> extends GenericFileAccessorHandle
680777
interface MappedDriveLetters {
681778
[driveLetter: string]: string
682779
}
780+
781+
function isFileShareSupportedOnCurrentPlatform(): boolean {
782+
// This is only supported on windows currently
783+
return isRunningInTest() || process.platform === 'win32'
784+
}

0 commit comments

Comments
 (0)