wilfredwake 1.0.9 → 1.1.0
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/.dockerignore +43 -0
- package/DOCKER.md +361 -0
- package/Dockerfile +59 -0
- package/Dockerfile.orchestrator +29 -0
- package/docker-compose.yml +74 -0
- package/package.json +1 -1
- package/src/config/services.yaml +5 -8
- package/src/orchestrator/orchestrator.js +154 -24
package/.dockerignore
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Git
|
|
2
|
+
.git
|
|
3
|
+
.gitignore
|
|
4
|
+
.github
|
|
5
|
+
|
|
6
|
+
# Dependencies
|
|
7
|
+
node_modules
|
|
8
|
+
npm-debug.log
|
|
9
|
+
yarn-error.log
|
|
10
|
+
package-lock.json
|
|
11
|
+
|
|
12
|
+
# Environment
|
|
13
|
+
.env
|
|
14
|
+
.env.local
|
|
15
|
+
.env.*.local
|
|
16
|
+
|
|
17
|
+
# IDE & Editor
|
|
18
|
+
.vscode
|
|
19
|
+
.idea
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
*~
|
|
23
|
+
.DS_Store
|
|
24
|
+
|
|
25
|
+
# Testing
|
|
26
|
+
tests
|
|
27
|
+
.test.js
|
|
28
|
+
|
|
29
|
+
# Documentation
|
|
30
|
+
README.md
|
|
31
|
+
QUICKSTART.md
|
|
32
|
+
SERVICE_STATUS_LOGIC.md
|
|
33
|
+
LICENSE
|
|
34
|
+
|
|
35
|
+
# Build artifacts
|
|
36
|
+
dist
|
|
37
|
+
build
|
|
38
|
+
out
|
|
39
|
+
|
|
40
|
+
# CI/CD
|
|
41
|
+
.gitlab-ci.yml
|
|
42
|
+
.circleci
|
|
43
|
+
.github/workflows
|
package/DOCKER.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# Docker Guide for wilfredwake
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This guide explains how to use Docker with wilfredwake. Three Docker configurations are provided:
|
|
6
|
+
|
|
7
|
+
- **Dockerfile** - Multi-stage build supporting production and development images
|
|
8
|
+
- **Dockerfile.orchestrator** - Lightweight orchestrator-only image
|
|
9
|
+
- **docker-compose.yml** - Orchestration for all services
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Build the Production Image
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
docker build -t wilfredwake:latest .
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Build the Orchestrator Image
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
docker build -f Dockerfile.orchestrator -t wilfredwake-orchestrator:latest .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Run with Docker Compose
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Start the orchestrator server
|
|
31
|
+
docker-compose up orchestrator
|
|
32
|
+
|
|
33
|
+
# Run CLI commands
|
|
34
|
+
docker-compose run --rm cli node bin/cli.js status
|
|
35
|
+
|
|
36
|
+
# Start development environment
|
|
37
|
+
docker-compose up dev
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Docker Images
|
|
43
|
+
|
|
44
|
+
### Main Dockerfile
|
|
45
|
+
|
|
46
|
+
Multi-stage Dockerfile with 3 targets:
|
|
47
|
+
|
|
48
|
+
#### **production** (default)
|
|
49
|
+
- Alpine-based Node.js 18
|
|
50
|
+
- Production dependencies only
|
|
51
|
+
- Optimized for size (~200MB)
|
|
52
|
+
- Includes health checks for orchestrator mode
|
|
53
|
+
|
|
54
|
+
**Build:**
|
|
55
|
+
```bash
|
|
56
|
+
docker build -t wilfredwake:latest .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Run:**
|
|
60
|
+
```bash
|
|
61
|
+
# Show help
|
|
62
|
+
docker run --rm wilfredwake:latest bin/cli.js --help
|
|
63
|
+
|
|
64
|
+
# Check status
|
|
65
|
+
docker run --rm -v $(pwd)/src/config/services.yaml:/app/src/config/services.yaml wilfredwake:latest bin/cli.js status
|
|
66
|
+
|
|
67
|
+
# Wake a service
|
|
68
|
+
docker run --rm -v $(pwd)/src/config/services.yaml:/app/src/config/services.yaml wilfredwake:latest bin/cli.js wake [service-name]
|
|
69
|
+
|
|
70
|
+
# Run orchestrator
|
|
71
|
+
docker run -p 3000:3000 -v $(pwd)/src/config/services.yaml:/app/src/config/services.yaml wilfredwake:latest src/orchestrator/server.js
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### **development**
|
|
75
|
+
- Alpine-based Node.js 18
|
|
76
|
+
- All dependencies (including devDependencies)
|
|
77
|
+
- Watch mode enabled (`--experimental-watch`)
|
|
78
|
+
- Full source code mounted via volumes
|
|
79
|
+
|
|
80
|
+
**Build:**
|
|
81
|
+
```bash
|
|
82
|
+
docker build --target development -t wilfredwake:dev .
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Run:**
|
|
86
|
+
```bash
|
|
87
|
+
docker run -v $(pwd):/app wilfredwake:dev --experimental-watch bin/cli.js --help
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Orchestrator Dockerfile
|
|
91
|
+
|
|
92
|
+
Lightweight image for just the orchestrator server:
|
|
93
|
+
|
|
94
|
+
**Build:**
|
|
95
|
+
```bash
|
|
96
|
+
docker build -f Dockerfile.orchestrator -t wilfredwake-orchestrator:latest .
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Run:**
|
|
100
|
+
```bash
|
|
101
|
+
docker run -p 3000:3000 \
|
|
102
|
+
-v $(pwd)/src/config/services.yaml:/app/src/config/services.yaml \
|
|
103
|
+
-e ORCHESTRATOR_PORT=3000 \
|
|
104
|
+
wilfredwake-orchestrator:latest
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Docker Compose
|
|
110
|
+
|
|
111
|
+
### Services
|
|
112
|
+
|
|
113
|
+
#### **orchestrator** (default)
|
|
114
|
+
REST API server running on port 3000
|
|
115
|
+
```bash
|
|
116
|
+
docker-compose up orchestrator
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### **cli**
|
|
120
|
+
Run CLI commands
|
|
121
|
+
```bash
|
|
122
|
+
docker-compose run --rm cli node bin/cli.js status
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### **dev**
|
|
126
|
+
Development environment with watch mode
|
|
127
|
+
```bash
|
|
128
|
+
docker-compose up dev
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Common Commands
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Start orchestrator
|
|
135
|
+
docker-compose up orchestrator
|
|
136
|
+
|
|
137
|
+
# Run a CLI command
|
|
138
|
+
docker-compose run --rm cli node bin/cli.js wake my-service
|
|
139
|
+
|
|
140
|
+
# View logs
|
|
141
|
+
docker-compose logs -f orchestrator
|
|
142
|
+
|
|
143
|
+
# Stop all services
|
|
144
|
+
docker-compose down
|
|
145
|
+
|
|
146
|
+
# Clean up volumes
|
|
147
|
+
docker-compose down -v
|
|
148
|
+
|
|
149
|
+
# Rebuild images
|
|
150
|
+
docker-compose up --build orchestrator
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Configuration
|
|
154
|
+
|
|
155
|
+
Edit `docker-compose.yml` to:
|
|
156
|
+
- Change ports
|
|
157
|
+
- Add environment variables
|
|
158
|
+
- Mount additional volumes
|
|
159
|
+
- Configure networking
|
|
160
|
+
|
|
161
|
+
Example - run orchestrator on port 5000:
|
|
162
|
+
```yaml
|
|
163
|
+
orchestrator:
|
|
164
|
+
...
|
|
165
|
+
ports:
|
|
166
|
+
- "5000:3000"
|
|
167
|
+
environment:
|
|
168
|
+
- ORCHESTRATOR_PORT=5000
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Environment Variables
|
|
174
|
+
|
|
175
|
+
### In Docker
|
|
176
|
+
|
|
177
|
+
Set via `docker run -e`:
|
|
178
|
+
```bash
|
|
179
|
+
docker run -e NODE_ENV=production -e ORCHESTRATOR_PORT=3000 wilfredwake:latest
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Or in `docker-compose.yml`:
|
|
183
|
+
```yaml
|
|
184
|
+
environment:
|
|
185
|
+
- NODE_ENV=production
|
|
186
|
+
- ORCHESTRATOR_PORT=3000
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Configuration Files
|
|
190
|
+
|
|
191
|
+
Mount your `services.yaml` and `.env`:
|
|
192
|
+
```bash
|
|
193
|
+
docker run -v /path/to/services.yaml:/app/src/config/services.yaml \
|
|
194
|
+
-v /path/to/.env:/app/.env \
|
|
195
|
+
wilfredwake:latest
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Health Checks
|
|
201
|
+
|
|
202
|
+
Both images include health checks for orchestrator mode:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Check if orchestrator is healthy
|
|
206
|
+
docker-compose ps orchestrator
|
|
207
|
+
|
|
208
|
+
# View health status
|
|
209
|
+
docker inspect wilfredwake-orchestrator | grep -A 10 '"Health"'
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Networking
|
|
215
|
+
|
|
216
|
+
### Within Docker Compose
|
|
217
|
+
Services communicate via `wilfredwake-network`:
|
|
218
|
+
```yaml
|
|
219
|
+
networks:
|
|
220
|
+
- wilfredwake-network
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Access orchestrator from CLI container:
|
|
224
|
+
```bash
|
|
225
|
+
http://orchestrator:3000
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### External Access
|
|
229
|
+
- Orchestrator exposed on `localhost:3000`
|
|
230
|
+
- CLI container doesn't expose ports (runs commands)
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Troubleshooting
|
|
235
|
+
|
|
236
|
+
### Build Issues
|
|
237
|
+
|
|
238
|
+
**Missing dependencies:**
|
|
239
|
+
```bash
|
|
240
|
+
docker build --no-cache -t wilfredwake:latest .
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Verify image contents:**
|
|
244
|
+
```bash
|
|
245
|
+
docker run -it --rm wilfredwake:latest sh
|
|
246
|
+
# Inside container:
|
|
247
|
+
# ls -la /app
|
|
248
|
+
# npm ls
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Runtime Issues
|
|
252
|
+
|
|
253
|
+
**Check logs:**
|
|
254
|
+
```bash
|
|
255
|
+
docker logs wilfredwake-orchestrator
|
|
256
|
+
docker-compose logs -f orchestrator
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Verify volume mounts:**
|
|
260
|
+
```bash
|
|
261
|
+
docker run -it --rm -v $(pwd)/src/config/services.yaml:/app/src/config/services.yaml wilfredwake:latest ls -la src/config/
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Debug networking:**
|
|
265
|
+
```bash
|
|
266
|
+
docker network inspect wilfredwake_wilfredwake-network
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Production Considerations
|
|
272
|
+
|
|
273
|
+
1. **Image Size**: Production image uses Alpine (~200MB)
|
|
274
|
+
2. **Security**: Uses non-root Node process
|
|
275
|
+
3. **Health Checks**: Built-in for orchestrator mode
|
|
276
|
+
4. **Logging**: Configure via environment variables
|
|
277
|
+
5. **Volumes**: Mount config files as read-only when possible
|
|
278
|
+
|
|
279
|
+
Example production deployment:
|
|
280
|
+
```bash
|
|
281
|
+
docker run -d \
|
|
282
|
+
--name wilfredwake-orchestrator \
|
|
283
|
+
--restart=unless-stopped \
|
|
284
|
+
-p 3000:3000 \
|
|
285
|
+
-v /etc/wilfredwake/services.yaml:/app/src/config/services.yaml:ro \
|
|
286
|
+
-v /etc/wilfredwake/.env:/app/.env:ro \
|
|
287
|
+
-e NODE_ENV=production \
|
|
288
|
+
wilfredwake:latest \
|
|
289
|
+
src/orchestrator/server.js
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## CI/CD Integration
|
|
295
|
+
|
|
296
|
+
### GitHub Actions Example
|
|
297
|
+
|
|
298
|
+
```yaml
|
|
299
|
+
- name: Build Docker image
|
|
300
|
+
run: docker build -t wilfredwake:${{ github.sha }} .
|
|
301
|
+
|
|
302
|
+
- name: Run tests in container
|
|
303
|
+
run: docker run --rm wilfredwake:latest npm test
|
|
304
|
+
|
|
305
|
+
- name: Push to registry
|
|
306
|
+
run: docker push registry.example.com/wilfredwake:${{ github.sha }}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Advanced Usage
|
|
312
|
+
|
|
313
|
+
### Custom Entrypoint
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
docker run --entrypoint /bin/sh -it wilfredwake:latest
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Multi-Container Setup
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# Terminal 1: Start orchestrator
|
|
323
|
+
docker-compose up orchestrator
|
|
324
|
+
|
|
325
|
+
# Terminal 2: Run CLI in same network
|
|
326
|
+
docker-compose run --rm cli node bin/cli.js health
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Using with Kubernetes
|
|
330
|
+
|
|
331
|
+
Create Kubernetes manifests based on these Docker images. Example deployment:
|
|
332
|
+
|
|
333
|
+
```yaml
|
|
334
|
+
apiVersion: v1
|
|
335
|
+
kind: Pod
|
|
336
|
+
metadata:
|
|
337
|
+
name: wilfredwake-orchestrator
|
|
338
|
+
spec:
|
|
339
|
+
containers:
|
|
340
|
+
- name: orchestrator
|
|
341
|
+
image: wilfredwake:latest
|
|
342
|
+
command: ["node", "src/orchestrator/server.js"]
|
|
343
|
+
ports:
|
|
344
|
+
- containerPort: 3000
|
|
345
|
+
volumeMounts:
|
|
346
|
+
- name: config
|
|
347
|
+
mountPath: /app/src/config
|
|
348
|
+
volumes:
|
|
349
|
+
- name: config
|
|
350
|
+
configMap:
|
|
351
|
+
name: wilfredwake-config
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Questions?
|
|
357
|
+
|
|
358
|
+
For more information, see:
|
|
359
|
+
- [README.md](README.md)
|
|
360
|
+
- [QUICKSTART.md](QUICKSTART.md)
|
|
361
|
+
- [SERVICE_STATUS_LOGIC.md](SERVICE_STATUS_LOGIC.md)
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Multi-stage build for wilfredwake
|
|
2
|
+
FROM node:18-alpine AS base
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Install dependencies stage
|
|
7
|
+
FROM base AS dependencies
|
|
8
|
+
COPY package*.json ./
|
|
9
|
+
RUN npm ci --only=production
|
|
10
|
+
|
|
11
|
+
# Development dependencies stage
|
|
12
|
+
FROM base AS dev-dependencies
|
|
13
|
+
COPY package*.json ./
|
|
14
|
+
RUN npm ci
|
|
15
|
+
|
|
16
|
+
# Production image
|
|
17
|
+
FROM base AS production
|
|
18
|
+
|
|
19
|
+
LABEL maintainer="Wilfred Wake"
|
|
20
|
+
LABEL description="CLI Tool for Multi-Developer Development Environment Wake & Status Management"
|
|
21
|
+
|
|
22
|
+
# Set environment to production
|
|
23
|
+
ENV NODE_ENV=production
|
|
24
|
+
ENV PATH="/app/node_modules/.bin:$PATH"
|
|
25
|
+
|
|
26
|
+
# Copy production dependencies
|
|
27
|
+
COPY --from=dependencies /app/node_modules ./node_modules
|
|
28
|
+
|
|
29
|
+
# Copy application code
|
|
30
|
+
COPY . .
|
|
31
|
+
|
|
32
|
+
# Make CLI executable
|
|
33
|
+
RUN chmod +x bin/cli.js
|
|
34
|
+
|
|
35
|
+
# Health check for orchestrator mode
|
|
36
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
37
|
+
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" || exit 1
|
|
38
|
+
|
|
39
|
+
# Default to CLI - can be overridden to run orchestrator server
|
|
40
|
+
ENTRYPOINT ["node"]
|
|
41
|
+
CMD ["bin/cli.js", "--help"]
|
|
42
|
+
|
|
43
|
+
# Development image
|
|
44
|
+
FROM base AS development
|
|
45
|
+
|
|
46
|
+
ENV NODE_ENV=development
|
|
47
|
+
ENV PATH="/app/node_modules/.bin:$PATH"
|
|
48
|
+
|
|
49
|
+
# Copy all dependencies (including devDependencies)
|
|
50
|
+
COPY --from=dev-dependencies /app/node_modules ./node_modules
|
|
51
|
+
|
|
52
|
+
# Copy application code
|
|
53
|
+
COPY . .
|
|
54
|
+
|
|
55
|
+
# Make CLI executable
|
|
56
|
+
RUN chmod +x bin/cli.js
|
|
57
|
+
|
|
58
|
+
ENTRYPOINT ["node"]
|
|
59
|
+
CMD ["--experimental-watch", "bin/cli.js", "--help"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Lightweight orchestrator-only image
|
|
2
|
+
FROM node:18-alpine
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
LABEL maintainer="Wilfred Wake"
|
|
7
|
+
LABEL description="Orchestrator API Server for wilfredwake"
|
|
8
|
+
|
|
9
|
+
ENV NODE_ENV=production
|
|
10
|
+
ENV ORCHESTRATOR_PORT=3000
|
|
11
|
+
ENV ORCHESTRATOR_HOST=0.0.0.0
|
|
12
|
+
ENV PATH="/app/node_modules/.bin:$PATH"
|
|
13
|
+
|
|
14
|
+
# Install dependencies
|
|
15
|
+
COPY package*.json ./
|
|
16
|
+
RUN npm ci --only=production && npm cache clean --force
|
|
17
|
+
|
|
18
|
+
# Copy application code
|
|
19
|
+
COPY src/ ./src/
|
|
20
|
+
COPY .env* ./
|
|
21
|
+
|
|
22
|
+
# Health check
|
|
23
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
24
|
+
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" || exit 1
|
|
25
|
+
|
|
26
|
+
EXPOSE 3000
|
|
27
|
+
|
|
28
|
+
ENTRYPOINT ["node"]
|
|
29
|
+
CMD ["src/orchestrator/server.js"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
version: '3.9'
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
# CLI container - for running wilfredwake commands
|
|
5
|
+
cli:
|
|
6
|
+
build:
|
|
7
|
+
context: .
|
|
8
|
+
dockerfile: Dockerfile
|
|
9
|
+
target: production
|
|
10
|
+
container_name: wilfredwake-cli
|
|
11
|
+
image: wilfredwake:latest
|
|
12
|
+
environment:
|
|
13
|
+
- NODE_ENV=production
|
|
14
|
+
volumes:
|
|
15
|
+
- ./src/config/services.yaml:/app/src/config/services.yaml
|
|
16
|
+
- ./.env:/app/.env
|
|
17
|
+
# Override to run specific commands
|
|
18
|
+
# command: node bin/cli.js status
|
|
19
|
+
# or: node bin/cli.js wake [service-name]
|
|
20
|
+
# or: node bin/cli.js health [service-name]
|
|
21
|
+
# or: node bin/cli.js init
|
|
22
|
+
profiles:
|
|
23
|
+
- cli
|
|
24
|
+
|
|
25
|
+
# Orchestrator server - for running the REST API
|
|
26
|
+
orchestrator:
|
|
27
|
+
build:
|
|
28
|
+
context: .
|
|
29
|
+
dockerfile: Dockerfile
|
|
30
|
+
target: production
|
|
31
|
+
container_name: wilfredwake-orchestrator
|
|
32
|
+
image: wilfredwake:latest
|
|
33
|
+
environment:
|
|
34
|
+
- NODE_ENV=production
|
|
35
|
+
- ORCHESTRATOR_PORT=3000
|
|
36
|
+
- ORCHESTRATOR_HOST=0.0.0.0
|
|
37
|
+
ports:
|
|
38
|
+
- "3000:3000"
|
|
39
|
+
volumes:
|
|
40
|
+
- ./src/config/services.yaml:/app/src/config/services.yaml
|
|
41
|
+
- ./.env:/app/.env
|
|
42
|
+
command: node src/orchestrator/server.js
|
|
43
|
+
healthcheck:
|
|
44
|
+
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"]
|
|
45
|
+
interval: 30s
|
|
46
|
+
timeout: 10s
|
|
47
|
+
retries: 3
|
|
48
|
+
start_period: 5s
|
|
49
|
+
profiles:
|
|
50
|
+
- orchestrator
|
|
51
|
+
- default
|
|
52
|
+
networks:
|
|
53
|
+
- wilfredwake-network
|
|
54
|
+
|
|
55
|
+
# Development container with watch mode
|
|
56
|
+
dev:
|
|
57
|
+
build:
|
|
58
|
+
context: .
|
|
59
|
+
dockerfile: Dockerfile
|
|
60
|
+
target: development
|
|
61
|
+
container_name: wilfredwake-dev
|
|
62
|
+
image: wilfredwake:dev
|
|
63
|
+
environment:
|
|
64
|
+
- NODE_ENV=development
|
|
65
|
+
volumes:
|
|
66
|
+
- .:/app
|
|
67
|
+
- /app/node_modules
|
|
68
|
+
command: node --experimental-watch bin/cli.js --help
|
|
69
|
+
profiles:
|
|
70
|
+
- dev
|
|
71
|
+
|
|
72
|
+
networks:
|
|
73
|
+
wilfredwake-network:
|
|
74
|
+
driver: bridge
|
package/package.json
CHANGED
package/src/config/services.yaml
CHANGED
|
@@ -25,7 +25,11 @@ services:
|
|
|
25
25
|
# Frontend service
|
|
26
26
|
frontend:
|
|
27
27
|
url: https://transactions-k6gk.onrender.com
|
|
28
|
-
|
|
28
|
+
# Root page (/) — this is a browser page URL. Orchestrator will
|
|
29
|
+
# treat a failed HTTP request as LIVE if a TCP connection to the
|
|
30
|
+
# host:port succeeds (covers cases where the page doesn't return
|
|
31
|
+
# a standard HTTP status but the site is up).
|
|
32
|
+
health: /
|
|
29
33
|
dependsOn: []
|
|
30
34
|
description: "frontend Service"
|
|
31
35
|
|
|
@@ -43,13 +47,6 @@ services:
|
|
|
43
47
|
dependsOn: []
|
|
44
48
|
description: "Notification Event Consumer"
|
|
45
49
|
|
|
46
|
-
# Notification producer
|
|
47
|
-
notification-producer:
|
|
48
|
-
url: https://notification-service-producer.onrender.com
|
|
49
|
-
health: /health
|
|
50
|
-
dependsOn: []
|
|
51
|
-
description: "Notification Producer Service"
|
|
52
|
-
|
|
53
50
|
# Staging environment (same services as dev)
|
|
54
51
|
staging: *dev_services
|
|
55
52
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import axios from 'axios';
|
|
10
|
+
import net from 'net';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Service state enumeration
|
|
@@ -74,29 +75,22 @@ export class Orchestrator {
|
|
|
74
75
|
// ASSUME ALL SERVICES ARE DEAD INITIALLY
|
|
75
76
|
// NEW: Instead of /wake endpoint, just check /health repeatedly
|
|
76
77
|
// ═══════════════════════════════════════════════════════════════
|
|
77
|
-
const results =
|
|
78
|
+
const results = new Map();
|
|
79
|
+
const serviceStartTimes = new Map();
|
|
78
80
|
const startTime = Date.now();
|
|
79
81
|
|
|
82
|
+
// Initialize state for each service and do an initial check
|
|
80
83
|
for (const service of services) {
|
|
81
|
-
|
|
84
|
+
this._logTimestamp(service.name, 'Wake initiated');
|
|
85
|
+
this._setServiceState(service.name, ServiceState.DEAD);
|
|
86
|
+
this._recordLastWakeTime(service.name);
|
|
87
|
+
serviceStartTimes.set(service.name, Date.now());
|
|
82
88
|
|
|
83
89
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
const status = await this._checkHealthWithTimeout(service, Math.min(timeout, 10));
|
|
91
|
+
const duration = Date.now() - serviceStartTimes.get(service.name);
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
this._setServiceState(service.name, ServiceState.DEAD);
|
|
89
|
-
this._recordLastWakeTime(service.name);
|
|
90
|
-
|
|
91
|
-
// Check health status (handles timeout internally)
|
|
92
|
-
const status = await this._checkHealthWithTimeout(
|
|
93
|
-
service,
|
|
94
|
-
timeout
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const duration = Date.now() - serviceStartTime;
|
|
98
|
-
|
|
99
|
-
results.push({
|
|
93
|
+
results.set(service.name, {
|
|
100
94
|
name: service.name,
|
|
101
95
|
status,
|
|
102
96
|
url: service.url,
|
|
@@ -107,9 +101,8 @@ export class Orchestrator {
|
|
|
107
101
|
|
|
108
102
|
this._setServiceState(service.name, status);
|
|
109
103
|
} catch (error) {
|
|
110
|
-
const duration = Date.now() -
|
|
111
|
-
|
|
112
|
-
results.push({
|
|
104
|
+
const duration = Date.now() - serviceStartTimes.get(service.name);
|
|
105
|
+
results.set(service.name, {
|
|
113
106
|
name: service.name,
|
|
114
107
|
status: ServiceState.FAILED,
|
|
115
108
|
url: service.url,
|
|
@@ -117,18 +110,92 @@ export class Orchestrator {
|
|
|
117
110
|
error: error.message,
|
|
118
111
|
lastWakeTime: this.lastWakeTime.get(service.name),
|
|
119
112
|
});
|
|
120
|
-
|
|
121
113
|
this._setServiceState(service.name, ServiceState.FAILED);
|
|
122
114
|
}
|
|
123
115
|
}
|
|
124
116
|
|
|
117
|
+
// If caller doesn't want to wait, return early with current statuses
|
|
118
|
+
const toArray = () => Array.from(results.values());
|
|
119
|
+
let allLive = toArray().every(r => r.status === ServiceState.LIVE);
|
|
120
|
+
if (!wait || allLive) {
|
|
121
|
+
const totalDuration = Date.now() - startTime;
|
|
122
|
+
return {
|
|
123
|
+
success: allLive,
|
|
124
|
+
error: null,
|
|
125
|
+
services: toArray(),
|
|
126
|
+
totalDuration,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Otherwise, poll remaining services until all are LIVE or timeout
|
|
131
|
+
const deadline = Date.now() + timeout * 1000;
|
|
132
|
+
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
// Check remaining services concurrently
|
|
135
|
+
const checks = [];
|
|
136
|
+
for (const service of services) {
|
|
137
|
+
const current = results.get(service.name);
|
|
138
|
+
if (current.status === ServiceState.LIVE) continue;
|
|
139
|
+
|
|
140
|
+
checks.push((async () => {
|
|
141
|
+
const health = await this._performHealthCheck(service);
|
|
142
|
+
if (health && health.state === ServiceState.LIVE) {
|
|
143
|
+
const duration = Date.now() - serviceStartTimes.get(service.name);
|
|
144
|
+
results.set(service.name, {
|
|
145
|
+
name: service.name,
|
|
146
|
+
status: ServiceState.LIVE,
|
|
147
|
+
url: service.url,
|
|
148
|
+
duration,
|
|
149
|
+
error: null,
|
|
150
|
+
lastWakeTime: this.lastWakeTime.get(service.name),
|
|
151
|
+
});
|
|
152
|
+
this._setServiceState(service.name, ServiceState.LIVE);
|
|
153
|
+
this._logTimestamp(service.name, `Became LIVE after ${duration}ms`);
|
|
154
|
+
} else if (health && health.state === ServiceState.DEAD) {
|
|
155
|
+
// still dead, update responseTime as duration
|
|
156
|
+
results.set(service.name, Object.assign({}, results.get(service.name), {
|
|
157
|
+
status: ServiceState.DEAD,
|
|
158
|
+
duration: Date.now() - serviceStartTimes.get(service.name),
|
|
159
|
+
error: health.error || results.get(service.name).error,
|
|
160
|
+
}));
|
|
161
|
+
} else if (health && health.state) {
|
|
162
|
+
// other intermediate state (waking/failed)
|
|
163
|
+
results.set(service.name, Object.assign({}, results.get(service.name), {
|
|
164
|
+
status: health.state,
|
|
165
|
+
duration: Date.now() - serviceStartTimes.get(service.name),
|
|
166
|
+
error: health.error || null,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
})());
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Wait for this round
|
|
173
|
+
await Promise.all(checks);
|
|
174
|
+
|
|
175
|
+
// Display progress summary
|
|
176
|
+
const summary = {
|
|
177
|
+
total: services.length,
|
|
178
|
+
live: toArray().filter(s => s.status === ServiceState.LIVE).length,
|
|
179
|
+
waking: toArray().filter(s => s.status === ServiceState.WAKING).length,
|
|
180
|
+
dead: toArray().filter(s => s.status === ServiceState.DEAD).length,
|
|
181
|
+
failed: toArray().filter(s => s.status === ServiceState.FAILED).length,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
this._logTimestamp('orchestrator', `Progress: ${summary.live}/${summary.total} live, ${summary.waking} waking, ${summary.dead} dead, ${summary.failed} failed`);
|
|
185
|
+
|
|
186
|
+
allLive = toArray().every(r => r.status === ServiceState.LIVE);
|
|
187
|
+
if (allLive) break;
|
|
188
|
+
|
|
189
|
+
// small delay before next round
|
|
190
|
+
await this._wait(1000);
|
|
191
|
+
}
|
|
192
|
+
|
|
125
193
|
const totalDuration = Date.now() - startTime;
|
|
126
|
-
const allLive = results.every(r => r.status === ServiceState.LIVE);
|
|
127
194
|
|
|
128
195
|
return {
|
|
129
|
-
success:
|
|
196
|
+
success: toArray().every(r => r.status === ServiceState.LIVE),
|
|
130
197
|
error: null,
|
|
131
|
-
services:
|
|
198
|
+
services: toArray(),
|
|
132
199
|
totalDuration,
|
|
133
200
|
};
|
|
134
201
|
}
|
|
@@ -163,6 +230,20 @@ export class Orchestrator {
|
|
|
163
230
|
status = health.state;
|
|
164
231
|
}
|
|
165
232
|
|
|
233
|
+
// Special rule: frontend may return an empty body or non-standard response
|
|
234
|
+
// that doesn't set a numeric statusCode in some environments. Per user
|
|
235
|
+
// requirement, treat the `frontend` service as LIVE if it returned no
|
|
236
|
+
// explicit error (i.e. health exists and no DEAD state). This ensures
|
|
237
|
+
// "nothing returned" from frontend is interpreted as live.
|
|
238
|
+
if (
|
|
239
|
+
service.name === 'frontend' &&
|
|
240
|
+
health &&
|
|
241
|
+
(health.statusCode == null) &&
|
|
242
|
+
health.state !== ServiceState.DEAD
|
|
243
|
+
) {
|
|
244
|
+
status = ServiceState.LIVE;
|
|
245
|
+
}
|
|
246
|
+
|
|
166
247
|
statusResults.push({
|
|
167
248
|
name: service.name,
|
|
168
249
|
status,
|
|
@@ -335,6 +416,55 @@ export class Orchestrator {
|
|
|
335
416
|
service.name,
|
|
336
417
|
`Health check failed: ${error.message} (DEAD - no response, needs waking)`
|
|
337
418
|
);
|
|
419
|
+
// If this is a frontend/page-style service (health is '/'), a
|
|
420
|
+
// failed HTTP request can still mean the site is "live" if the
|
|
421
|
+
// host accepts TCP connections. Try a TCP connect to the host:port
|
|
422
|
+
// before marking it DEAD. This covers cases where the page itself
|
|
423
|
+
// doesn't return a normal HTTP status but the server is responsive
|
|
424
|
+
// (stops loading in a browser).
|
|
425
|
+
try {
|
|
426
|
+
if (service.health === '/' || service.health === '') {
|
|
427
|
+
const parsed = new URL(service.url);
|
|
428
|
+
const port = parsed.port ? parseInt(parsed.port, 10) : (parsed.protocol === 'https:' ? 443 : 80);
|
|
429
|
+
const host = parsed.hostname;
|
|
430
|
+
|
|
431
|
+
this._logTimestamp(service.name, `HTTP failed; attempting TCP connect to ${host}:${port}`);
|
|
432
|
+
|
|
433
|
+
const tcpStart = Date.now();
|
|
434
|
+
const tcpOk = await new Promise((resolve) => {
|
|
435
|
+
const socket = new net.Socket();
|
|
436
|
+
let settled = false;
|
|
437
|
+
|
|
438
|
+
const onCleanup = (ok) => {
|
|
439
|
+
if (settled) return;
|
|
440
|
+
settled = true;
|
|
441
|
+
try { socket.destroy(); } catch (e) {}
|
|
442
|
+
resolve(ok);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
socket.setTimeout(3000);
|
|
446
|
+
socket.once('connect', () => onCleanup(true));
|
|
447
|
+
socket.once('timeout', () => onCleanup(false));
|
|
448
|
+
socket.once('error', () => onCleanup(false));
|
|
449
|
+
socket.connect(port, host);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const tcpResponseTime = Date.now() - tcpStart;
|
|
453
|
+
|
|
454
|
+
if (tcpOk) {
|
|
455
|
+
this._logTimestamp(service.name, `TCP connect succeeded in ${tcpResponseTime}ms - treating as LIVE`);
|
|
456
|
+
return {
|
|
457
|
+
state: ServiceState.LIVE,
|
|
458
|
+
statusCode: null,
|
|
459
|
+
responseTime: tcpResponseTime,
|
|
460
|
+
error: null,
|
|
461
|
+
uptime: null,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch (tcpErr) {
|
|
466
|
+
this._logTimestamp(service.name, `TCP fallback failed: ${tcpErr.message}`);
|
|
467
|
+
}
|
|
338
468
|
|
|
339
469
|
return {
|
|
340
470
|
state: ServiceState.DEAD, // No response received = service needs waking
|