@insureco/cli 0.1.1 → 0.1.4
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/dist/commands/git.d.ts +30 -0
- package/dist/commands/git.d.ts.map +1 -0
- package/dist/commands/git.js +300 -0
- package/dist/commands/git.js.map +1 -0
- package/dist/commands/oauth.d.ts +33 -0
- package/dist/commands/oauth.d.ts.map +1 -0
- package/dist/commands/oauth.js +181 -0
- package/dist/commands/oauth.js.map +1 -0
- package/dist/commands/user.d.ts +13 -0
- package/dist/commands/user.d.ts.map +1 -0
- package/dist/commands/user.js +96 -0
- package/dist/commands/user.js.map +1 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/bio.d.ts +13 -0
- package/dist/lib/bio.d.ts.map +1 -0
- package/dist/lib/bio.js +53 -0
- package/dist/lib/bio.js.map +1 -0
- package/dist/lib/builder.d.ts +17 -1
- package/dist/lib/builder.d.ts.map +1 -1
- package/dist/lib/builder.js +53 -2
- package/dist/lib/builder.js.map +1 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/forgejo.d.ts +149 -0
- package/dist/lib/forgejo.d.ts.map +1 -0
- package/dist/lib/forgejo.js +137 -0
- package/dist/lib/forgejo.js.map +1 -0
- package/dist/templates/api/.env.example +15 -0
- package/dist/templates/api/Dockerfile +45 -0
- package/dist/templates/api/README.md +85 -0
- package/dist/templates/api/catalog-info.yaml +51 -0
- package/dist/templates/api/helm/{{name}}/Chart.yaml +9 -0
- package/dist/templates/api/helm/{{name}}/templates/deployment.yaml +68 -0
- package/dist/templates/api/helm/{{name}}/templates/service.yaml +17 -0
- package/dist/templates/api/helm/{{name}}/values.yaml +55 -0
- package/dist/templates/api/package.json +33 -0
- package/dist/templates/api/src/index.ts +61 -0
- package/dist/templates/api/src/routes/example.ts +162 -0
- package/dist/templates/api/tsconfig.json +18 -0
- package/dist/templates/crosspod/README.md +87 -0
- package/dist/templates/crosspod/service-a/Dockerfile +27 -0
- package/dist/templates/crosspod/service-a/catalog-info.yaml +54 -0
- package/dist/templates/crosspod/service-a/helm/{{name}}-service-a/Chart.yaml +6 -0
- package/dist/templates/crosspod/service-a/helm/{{name}}-service-a/values.yaml +35 -0
- package/dist/templates/crosspod/service-a/package.json +29 -0
- package/dist/templates/crosspod/service-a/src/index.ts +89 -0
- package/dist/templates/crosspod/service-a/tsconfig.json +14 -0
- package/dist/templates/crosspod/service-b/Dockerfile +27 -0
- package/dist/templates/crosspod/service-b/catalog-info.yaml +27 -0
- package/dist/templates/crosspod/service-b/helm/{{name}}-service-b/Chart.yaml +6 -0
- package/dist/templates/crosspod/service-b/helm/{{name}}-service-b/values.yaml +38 -0
- package/dist/templates/crosspod/service-b/package.json +26 -0
- package/dist/templates/crosspod/service-b/src/index.ts +143 -0
- package/dist/templates/crosspod/service-b/tsconfig.json +14 -0
- package/dist/templates/nextjs/.env.example +11 -0
- package/dist/templates/nextjs/Dockerfile +51 -0
- package/dist/templates/nextjs/README.md +87 -0
- package/dist/templates/nextjs/catalog-info.yaml +16 -0
- package/dist/templates/nextjs/helm/{{name}}/Chart.yaml +9 -0
- package/dist/templates/nextjs/helm/{{name}}/templates/deployment.yaml +68 -0
- package/dist/templates/nextjs/helm/{{name}}/templates/service.yaml +17 -0
- package/dist/templates/nextjs/helm/{{name}}/values.yaml +51 -0
- package/dist/templates/nextjs/next.config.js +23 -0
- package/dist/templates/nextjs/package.json +29 -0
- package/dist/templates/nextjs/public/.gitkeep +0 -0
- package/dist/templates/nextjs/src/app/api/example/route.ts +63 -0
- package/dist/templates/nextjs/src/app/api/health/route.ts +10 -0
- package/dist/templates/nextjs/src/app/layout.tsx +18 -0
- package/dist/templates/nextjs/src/app/page.tsx +49 -0
- package/dist/templates/nextjs/tsconfig.json +26 -0
- package/dist/templates/worker/.env.example +13 -0
- package/dist/templates/worker/Dockerfile +26 -0
- package/dist/templates/worker/README.md +106 -0
- package/dist/templates/worker/catalog-info.yaml +19 -0
- package/dist/templates/worker/helm/{{name}}/Chart.yaml +9 -0
- package/dist/templates/worker/helm/{{name}}/templates/deployment.yaml +64 -0
- package/dist/templates/worker/helm/{{name}}/values.yaml +55 -0
- package/dist/templates/worker/package.json +26 -0
- package/dist/templates/worker/src/index.ts +185 -0
- package/dist/templates/worker/tsconfig.json +14 -0
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import express, { type Request, type Response, type NextFunction } from 'express'
|
|
2
|
+
import cors from 'cors'
|
|
3
|
+
import helmet from 'helmet'
|
|
4
|
+
import { itemsRouter } from './routes/example.js'
|
|
5
|
+
|
|
6
|
+
const app = express()
|
|
7
|
+
const PORT = process.env.PORT || 3000
|
|
8
|
+
|
|
9
|
+
// Middleware
|
|
10
|
+
app.use(helmet())
|
|
11
|
+
app.use(cors())
|
|
12
|
+
app.use(express.json())
|
|
13
|
+
|
|
14
|
+
// Request logging
|
|
15
|
+
app.use((req: Request, _res: Response, next: NextFunction) => {
|
|
16
|
+
const timestamp = new Date().toISOString()
|
|
17
|
+
process.stdout.write(`[${timestamp}] ${req.method} ${req.path}\n`)
|
|
18
|
+
next()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Health check endpoint
|
|
22
|
+
app.get('/health', (_req: Request, res: Response) => {
|
|
23
|
+
res.json({
|
|
24
|
+
status: 'healthy',
|
|
25
|
+
service: '{{name}}',
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
uptime: process.uptime(),
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// API routes
|
|
32
|
+
app.use('/api/items', itemsRouter)
|
|
33
|
+
|
|
34
|
+
// 404 handler
|
|
35
|
+
app.use((_req: Request, res: Response) => {
|
|
36
|
+
res.status(404).json({
|
|
37
|
+
success: false,
|
|
38
|
+
error: {
|
|
39
|
+
code: 'NOT_FOUND',
|
|
40
|
+
message: 'The requested resource was not found',
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Error handler
|
|
46
|
+
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
|
47
|
+
process.stderr.write(`Error: ${err.message}\n`)
|
|
48
|
+
res.status(500).json({
|
|
49
|
+
success: false,
|
|
50
|
+
error: {
|
|
51
|
+
code: 'INTERNAL_ERROR',
|
|
52
|
+
message: process.env.NODE_ENV === 'production'
|
|
53
|
+
? 'An internal error occurred'
|
|
54
|
+
: err.message,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
app.listen(PORT, () => {
|
|
60
|
+
process.stdout.write(`{{name}} listening on port ${PORT}\n`)
|
|
61
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Router, type Request, type Response } from 'express'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
const router = Router()
|
|
5
|
+
|
|
6
|
+
// In-memory storage for demo purposes
|
|
7
|
+
interface Item {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
description: string
|
|
11
|
+
createdAt: string
|
|
12
|
+
updatedAt: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const items: Map<string, Item> = new Map()
|
|
16
|
+
|
|
17
|
+
// Validation schemas
|
|
18
|
+
const CreateItemSchema = z.object({
|
|
19
|
+
name: z.string().min(1).max(100),
|
|
20
|
+
description: z.string().max(500).optional().default(''),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const UpdateItemSchema = z.object({
|
|
24
|
+
name: z.string().min(1).max(100).optional(),
|
|
25
|
+
description: z.string().max(500).optional(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Generate unique ID
|
|
29
|
+
function generateId(): string {
|
|
30
|
+
return Date.now().toString(36) + Math.random().toString(36).substring(2)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// List all items
|
|
34
|
+
router.get('/', (_req: Request, res: Response) => {
|
|
35
|
+
const itemList = Array.from(items.values())
|
|
36
|
+
res.json({
|
|
37
|
+
success: true,
|
|
38
|
+
data: itemList,
|
|
39
|
+
meta: {
|
|
40
|
+
total: itemList.length,
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Get single item
|
|
47
|
+
router.get('/:id', (req: Request, res: Response) => {
|
|
48
|
+
const item = items.get(req.params.id)
|
|
49
|
+
|
|
50
|
+
if (!item) {
|
|
51
|
+
res.status(404).json({
|
|
52
|
+
success: false,
|
|
53
|
+
error: {
|
|
54
|
+
code: 'NOT_FOUND',
|
|
55
|
+
message: 'Item not found',
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
res.json({
|
|
62
|
+
success: true,
|
|
63
|
+
data: item,
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Create item
|
|
68
|
+
router.post('/', (req: Request, res: Response) => {
|
|
69
|
+
const result = CreateItemSchema.safeParse(req.body)
|
|
70
|
+
|
|
71
|
+
if (!result.success) {
|
|
72
|
+
res.status(400).json({
|
|
73
|
+
success: false,
|
|
74
|
+
error: {
|
|
75
|
+
code: 'VALIDATION_ERROR',
|
|
76
|
+
message: 'Invalid request body',
|
|
77
|
+
details: result.error.flatten(),
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const now = new Date().toISOString()
|
|
84
|
+
const item: Item = {
|
|
85
|
+
id: generateId(),
|
|
86
|
+
name: result.data.name,
|
|
87
|
+
description: result.data.description,
|
|
88
|
+
createdAt: now,
|
|
89
|
+
updatedAt: now,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
items.set(item.id, item)
|
|
93
|
+
|
|
94
|
+
res.status(201).json({
|
|
95
|
+
success: true,
|
|
96
|
+
data: item,
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Update item
|
|
101
|
+
router.put('/:id', (req: Request, res: Response) => {
|
|
102
|
+
const existing = items.get(req.params.id)
|
|
103
|
+
|
|
104
|
+
if (!existing) {
|
|
105
|
+
res.status(404).json({
|
|
106
|
+
success: false,
|
|
107
|
+
error: {
|
|
108
|
+
code: 'NOT_FOUND',
|
|
109
|
+
message: 'Item not found',
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = UpdateItemSchema.safeParse(req.body)
|
|
116
|
+
|
|
117
|
+
if (!result.success) {
|
|
118
|
+
res.status(400).json({
|
|
119
|
+
success: false,
|
|
120
|
+
error: {
|
|
121
|
+
code: 'VALIDATION_ERROR',
|
|
122
|
+
message: 'Invalid request body',
|
|
123
|
+
details: result.error.flatten(),
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const updated: Item = {
|
|
130
|
+
...existing,
|
|
131
|
+
name: result.data.name ?? existing.name,
|
|
132
|
+
description: result.data.description ?? existing.description,
|
|
133
|
+
updatedAt: new Date().toISOString(),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
items.set(updated.id, updated)
|
|
137
|
+
|
|
138
|
+
res.json({
|
|
139
|
+
success: true,
|
|
140
|
+
data: updated,
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Delete item
|
|
145
|
+
router.delete('/:id', (req: Request, res: Response) => {
|
|
146
|
+
const existed = items.delete(req.params.id)
|
|
147
|
+
|
|
148
|
+
if (!existed) {
|
|
149
|
+
res.status(404).json({
|
|
150
|
+
success: false,
|
|
151
|
+
error: {
|
|
152
|
+
code: 'NOT_FOUND',
|
|
153
|
+
message: 'Item not found',
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
res.status(204).send()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
export { router as itemsRouter }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# {{name}} - Pod-to-Pod Communication Demo
|
|
2
|
+
|
|
3
|
+
This template demonstrates service-to-service (pod-to-pod) communication within the Tawa platform.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────┐
|
|
9
|
+
│ Client │
|
|
10
|
+
└────────┬────────┘
|
|
11
|
+
│
|
|
12
|
+
┌────────▼────────┐
|
|
13
|
+
│ Janus API │
|
|
14
|
+
│ Gateway │
|
|
15
|
+
└────────┬────────┘
|
|
16
|
+
│
|
|
17
|
+
┌───────────────────┼───────────────────┐
|
|
18
|
+
│ │ │
|
|
19
|
+
┌────────▼────────┐ ┌────────▼────────┐ │
|
|
20
|
+
│ service-a │ │ service-b │ │
|
|
21
|
+
│ (Provider) │ │ (Consumer) │ │
|
|
22
|
+
│ │ │ │ │
|
|
23
|
+
│ Exposes data │ │ Calls service-a │ │
|
|
24
|
+
│ via REST API │ │ internally │ │
|
|
25
|
+
└─────────────────┘ └────────┬────────┘ │
|
|
26
|
+
│ │
|
|
27
|
+
┌────────▼────────┐ │
|
|
28
|
+
│ Internal K8s │◄────────┘
|
|
29
|
+
│ Service DNS │
|
|
30
|
+
└─────────────────┘
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Services
|
|
34
|
+
|
|
35
|
+
### service-a (Provider)
|
|
36
|
+
|
|
37
|
+
- Exposes data via REST API
|
|
38
|
+
- Routes configured with `auth: service` for internal-only access
|
|
39
|
+
- Accessible at `http://{{name}}-service-a:3000` within the cluster
|
|
40
|
+
|
|
41
|
+
### service-b (Consumer)
|
|
42
|
+
|
|
43
|
+
- Calls service-a to retrieve and process data
|
|
44
|
+
- Uses internal Kubernetes DNS for service discovery
|
|
45
|
+
- Demonstrates service-to-service authentication
|
|
46
|
+
|
|
47
|
+
## Pod-to-Pod Authentication
|
|
48
|
+
|
|
49
|
+
When `auth: service` is set on a route in `catalog-info.yaml`:
|
|
50
|
+
|
|
51
|
+
1. Requests must include service credentials
|
|
52
|
+
2. Janus validates the calling service identity
|
|
53
|
+
3. Only registered services can access protected endpoints
|
|
54
|
+
|
|
55
|
+
### Example catalog-info.yaml configuration:
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
spec:
|
|
59
|
+
routes:
|
|
60
|
+
- path: /api/internal/*
|
|
61
|
+
methods: [GET, POST]
|
|
62
|
+
auth: service # Only other services can call this
|
|
63
|
+
scopes:
|
|
64
|
+
- read:data
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Deployment
|
|
68
|
+
|
|
69
|
+
Deploy both services:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cd {{name}}/service-a
|
|
73
|
+
iec link && iec deploy
|
|
74
|
+
|
|
75
|
+
cd ../service-b
|
|
76
|
+
iec link && iec deploy
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Testing
|
|
80
|
+
|
|
81
|
+
1. Deploy both services to sandbox
|
|
82
|
+
2. Call service-b's `/api/data` endpoint
|
|
83
|
+
3. service-b will internally call service-a and return combined data
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
curl https://{{name}}-service-b.sandbox.tawa.insureco.io/api/data
|
|
87
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
FROM node:22-alpine AS builder
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
COPY package*.json ./
|
|
4
|
+
RUN npm ci
|
|
5
|
+
COPY . .
|
|
6
|
+
RUN npm run build
|
|
7
|
+
|
|
8
|
+
FROM node:22-alpine
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
COPY package*.json ./
|
|
11
|
+
RUN npm ci --production && npm cache clean --force
|
|
12
|
+
COPY --from=builder /app/dist ./dist
|
|
13
|
+
|
|
14
|
+
ENV NODE_ENV=production
|
|
15
|
+
ENV PORT=3000
|
|
16
|
+
|
|
17
|
+
RUN addgroup -g 1001 -S nodejs && \
|
|
18
|
+
adduser -S nodejs -u 1001 && \
|
|
19
|
+
chown -R nodejs:nodejs /app
|
|
20
|
+
|
|
21
|
+
USER nodejs
|
|
22
|
+
EXPOSE 3000
|
|
23
|
+
|
|
24
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
25
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
|
26
|
+
|
|
27
|
+
CMD ["node", "dist/index.js"]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
apiVersion: backstage.io/v1alpha1
|
|
2
|
+
kind: Component
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{name}}-service-a
|
|
5
|
+
description: Provider service for {{name}} - exposes internal data API
|
|
6
|
+
tags:
|
|
7
|
+
- tawa
|
|
8
|
+
- api
|
|
9
|
+
- provider
|
|
10
|
+
annotations:
|
|
11
|
+
insureco.io/framework: express
|
|
12
|
+
insureco.io/language: typescript
|
|
13
|
+
spec:
|
|
14
|
+
type: service
|
|
15
|
+
lifecycle: experimental
|
|
16
|
+
owner: team-platform
|
|
17
|
+
providesApis:
|
|
18
|
+
- {{name}}-service-a-api
|
|
19
|
+
routes:
|
|
20
|
+
- path: /health
|
|
21
|
+
methods: [GET]
|
|
22
|
+
auth: none
|
|
23
|
+
description: Health check endpoint
|
|
24
|
+
- path: /api/internal/*
|
|
25
|
+
methods: [GET, POST]
|
|
26
|
+
auth: service
|
|
27
|
+
scopes:
|
|
28
|
+
- read:data
|
|
29
|
+
- write:data
|
|
30
|
+
description: Internal API - service-to-service only
|
|
31
|
+
---
|
|
32
|
+
apiVersion: backstage.io/v1alpha1
|
|
33
|
+
kind: API
|
|
34
|
+
metadata:
|
|
35
|
+
name: {{name}}-service-a-api
|
|
36
|
+
description: Internal data API for {{name}}
|
|
37
|
+
spec:
|
|
38
|
+
type: openapi
|
|
39
|
+
lifecycle: experimental
|
|
40
|
+
owner: team-platform
|
|
41
|
+
definition: |
|
|
42
|
+
openapi: 3.0.0
|
|
43
|
+
info:
|
|
44
|
+
title: {{name}} Service A API
|
|
45
|
+
version: 1.0.0
|
|
46
|
+
paths:
|
|
47
|
+
/api/internal/items:
|
|
48
|
+
get:
|
|
49
|
+
summary: Get all items (service-to-service only)
|
|
50
|
+
security:
|
|
51
|
+
- serviceAuth: []
|
|
52
|
+
responses:
|
|
53
|
+
'200':
|
|
54
|
+
description: List of items
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
replicaCount: 1
|
|
2
|
+
|
|
3
|
+
image:
|
|
4
|
+
repository: registry.digitalocean.com/insureco/{{name}}-service-a
|
|
5
|
+
tag: latest
|
|
6
|
+
pullPolicy: IfNotPresent
|
|
7
|
+
|
|
8
|
+
imagePullSecrets:
|
|
9
|
+
- name: do-registry
|
|
10
|
+
|
|
11
|
+
service:
|
|
12
|
+
type: ClusterIP
|
|
13
|
+
port: 3000
|
|
14
|
+
|
|
15
|
+
resources:
|
|
16
|
+
limits:
|
|
17
|
+
cpu: 200m
|
|
18
|
+
memory: 256Mi
|
|
19
|
+
requests:
|
|
20
|
+
cpu: 50m
|
|
21
|
+
memory: 64Mi
|
|
22
|
+
|
|
23
|
+
livenessProbe:
|
|
24
|
+
httpGet:
|
|
25
|
+
path: /health
|
|
26
|
+
port: 3000
|
|
27
|
+
initialDelaySeconds: 10
|
|
28
|
+
periodSeconds: 10
|
|
29
|
+
|
|
30
|
+
readinessProbe:
|
|
31
|
+
httpGet:
|
|
32
|
+
path: /health
|
|
33
|
+
port: 3000
|
|
34
|
+
initialDelaySeconds: 5
|
|
35
|
+
periodSeconds: 5
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}-service-a",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Provider service - exposes internal data API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"dev": "tsx watch src/index.ts",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"lint": "eslint src --ext .ts",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"express": "^4.21.0",
|
|
15
|
+
"cors": "^2.8.5",
|
|
16
|
+
"helmet": "^8.0.0",
|
|
17
|
+
"zod": "^3.24.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/cors": "^2.8.17",
|
|
21
|
+
"@types/express": "^5.0.0",
|
|
22
|
+
"@types/node": "^22.0.0",
|
|
23
|
+
"tsx": "^4.0.0",
|
|
24
|
+
"typescript": "^5.7.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import express, { type Request, type Response, type NextFunction } from 'express'
|
|
2
|
+
import helmet from 'helmet'
|
|
3
|
+
|
|
4
|
+
const app = express()
|
|
5
|
+
const PORT = process.env.PORT || 3000
|
|
6
|
+
const SERVICE_NAME = '{{name}}-service-a'
|
|
7
|
+
|
|
8
|
+
// Middleware
|
|
9
|
+
app.use(helmet())
|
|
10
|
+
app.use(express.json())
|
|
11
|
+
|
|
12
|
+
// Sample data store
|
|
13
|
+
interface Item {
|
|
14
|
+
id: string
|
|
15
|
+
name: string
|
|
16
|
+
value: number
|
|
17
|
+
source: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const items: Item[] = [
|
|
21
|
+
{ id: '1', name: 'Widget A', value: 100, source: SERVICE_NAME },
|
|
22
|
+
{ id: '2', name: 'Widget B', value: 200, source: SERVICE_NAME },
|
|
23
|
+
{ id: '3', name: 'Widget C', value: 300, source: SERVICE_NAME },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
// Request logging with caller identification
|
|
27
|
+
app.use((req: Request, _res: Response, next: NextFunction) => {
|
|
28
|
+
const caller = req.headers['x-service-name'] || 'unknown'
|
|
29
|
+
const timestamp = new Date().toISOString()
|
|
30
|
+
process.stdout.write(`[${timestamp}] ${req.method} ${req.path} (caller: ${caller})\n`)
|
|
31
|
+
next()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Health check - public
|
|
35
|
+
app.get('/health', (_req: Request, res: Response) => {
|
|
36
|
+
res.json({
|
|
37
|
+
status: 'healthy',
|
|
38
|
+
service: SERVICE_NAME,
|
|
39
|
+
timestamp: new Date().toISOString(),
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Internal API - service-to-service only
|
|
44
|
+
// In production, Janus validates the auth: service requirement
|
|
45
|
+
app.get('/api/internal/items', (req: Request, res: Response) => {
|
|
46
|
+
// Log the service-to-service call
|
|
47
|
+
const callerService = req.headers['x-service-name'] as string
|
|
48
|
+
process.stdout.write(`Internal API called by: ${callerService || 'direct'}\n`)
|
|
49
|
+
|
|
50
|
+
res.json({
|
|
51
|
+
success: true,
|
|
52
|
+
data: items,
|
|
53
|
+
meta: {
|
|
54
|
+
source: SERVICE_NAME,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
app.get('/api/internal/items/:id', (req: Request, res: Response) => {
|
|
61
|
+
const item = items.find(i => i.id === req.params.id)
|
|
62
|
+
|
|
63
|
+
if (!item) {
|
|
64
|
+
res.status(404).json({
|
|
65
|
+
success: false,
|
|
66
|
+
error: { code: 'NOT_FOUND', message: 'Item not found' },
|
|
67
|
+
})
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
res.json({
|
|
72
|
+
success: true,
|
|
73
|
+
data: item,
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Error handler
|
|
78
|
+
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
|
79
|
+
process.stderr.write(`Error: ${err.message}\n`)
|
|
80
|
+
res.status(500).json({
|
|
81
|
+
success: false,
|
|
82
|
+
error: { code: 'INTERNAL_ERROR', message: 'Internal error' },
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
app.listen(PORT, () => {
|
|
87
|
+
process.stdout.write(`${SERVICE_NAME} listening on port ${PORT}\n`)
|
|
88
|
+
process.stdout.write(`This service exposes internal APIs for service-to-service communication\n`)
|
|
89
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*"],
|
|
13
|
+
"exclude": ["node_modules", "dist"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
FROM node:22-alpine AS builder
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
COPY package*.json ./
|
|
4
|
+
RUN npm ci
|
|
5
|
+
COPY . .
|
|
6
|
+
RUN npm run build
|
|
7
|
+
|
|
8
|
+
FROM node:22-alpine
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
COPY package*.json ./
|
|
11
|
+
RUN npm ci --production && npm cache clean --force
|
|
12
|
+
COPY --from=builder /app/dist ./dist
|
|
13
|
+
|
|
14
|
+
ENV NODE_ENV=production
|
|
15
|
+
ENV PORT=3000
|
|
16
|
+
|
|
17
|
+
RUN addgroup -g 1001 -S nodejs && \
|
|
18
|
+
adduser -S nodejs -u 1001 && \
|
|
19
|
+
chown -R nodejs:nodejs /app
|
|
20
|
+
|
|
21
|
+
USER nodejs
|
|
22
|
+
EXPOSE 3000
|
|
23
|
+
|
|
24
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
25
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
|
26
|
+
|
|
27
|
+
CMD ["node", "dist/index.js"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
apiVersion: backstage.io/v1alpha1
|
|
2
|
+
kind: Component
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{name}}-service-b
|
|
5
|
+
description: Consumer service for {{name}} - calls service-a internally
|
|
6
|
+
tags:
|
|
7
|
+
- tawa
|
|
8
|
+
- api
|
|
9
|
+
- consumer
|
|
10
|
+
annotations:
|
|
11
|
+
insureco.io/framework: express
|
|
12
|
+
insureco.io/language: typescript
|
|
13
|
+
spec:
|
|
14
|
+
type: service
|
|
15
|
+
lifecycle: experimental
|
|
16
|
+
owner: team-platform
|
|
17
|
+
consumesApis:
|
|
18
|
+
- {{name}}-service-a-api
|
|
19
|
+
routes:
|
|
20
|
+
- path: /health
|
|
21
|
+
methods: [GET]
|
|
22
|
+
auth: none
|
|
23
|
+
description: Health check endpoint
|
|
24
|
+
- path: /api/*
|
|
25
|
+
methods: [GET, POST]
|
|
26
|
+
auth: required
|
|
27
|
+
description: Public API that aggregates data from service-a
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
replicaCount: 1
|
|
2
|
+
|
|
3
|
+
image:
|
|
4
|
+
repository: registry.digitalocean.com/insureco/{{name}}-service-b
|
|
5
|
+
tag: latest
|
|
6
|
+
pullPolicy: IfNotPresent
|
|
7
|
+
|
|
8
|
+
imagePullSecrets:
|
|
9
|
+
- name: do-registry
|
|
10
|
+
|
|
11
|
+
service:
|
|
12
|
+
type: ClusterIP
|
|
13
|
+
port: 3000
|
|
14
|
+
|
|
15
|
+
resources:
|
|
16
|
+
limits:
|
|
17
|
+
cpu: 200m
|
|
18
|
+
memory: 256Mi
|
|
19
|
+
requests:
|
|
20
|
+
cpu: 50m
|
|
21
|
+
memory: 64Mi
|
|
22
|
+
|
|
23
|
+
env:
|
|
24
|
+
SERVICE_A_URL: "http://{{name}}-service-a:3000"
|
|
25
|
+
|
|
26
|
+
livenessProbe:
|
|
27
|
+
httpGet:
|
|
28
|
+
path: /health
|
|
29
|
+
port: 3000
|
|
30
|
+
initialDelaySeconds: 10
|
|
31
|
+
periodSeconds: 10
|
|
32
|
+
|
|
33
|
+
readinessProbe:
|
|
34
|
+
httpGet:
|
|
35
|
+
path: /health
|
|
36
|
+
port: 3000
|
|
37
|
+
initialDelaySeconds: 5
|
|
38
|
+
periodSeconds: 5
|