@empire-builder-kit/containers 0.0.1-alpha.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/README.md +20 -0
- package/dist/executors.json +3 -0
- package/dist/generators/service/files-go/Dockerfile.template +11 -0
- package/dist/generators/service/files-go/README.md.template +11 -0
- package/dist/generators/service/files-go/cmd/main.go.template +34 -0
- package/dist/generators/service/files-go/docs/contracts.md.template +9 -0
- package/dist/generators/service/files-go/go.mod.template +3 -0
- package/dist/generators/service/files-python/Dockerfile.template +12 -0
- package/dist/generators/service/files-python/README.md.template +11 -0
- package/dist/generators/service/files-python/docs/contracts.md.template +9 -0
- package/dist/generators/service/files-python/pyproject.toml.template +15 -0
- package/dist/generators/service/files-python/src/main.py.template +20 -0
- package/dist/generators/service/files-rust/Cargo.toml.template +16 -0
- package/dist/generators/service/files-rust/Dockerfile.template +12 -0
- package/dist/generators/service/files-rust/README.md.template +11 -0
- package/dist/generators/service/files-rust/docs/contracts.md.template +9 -0
- package/dist/generators/service/files-rust/src/main.rs.template +29 -0
- package/dist/generators/service/files-typescript/Dockerfile.template +30 -0
- package/dist/generators/service/files-typescript/README.md.template +24 -0
- package/dist/generators/service/files-typescript/docs/architecture.md.template +18 -0
- package/dist/generators/service/files-typescript/docs/contracts.md.template +20 -0
- package/dist/generators/service/files-typescript/docs/operations.md.template +24 -0
- package/dist/generators/service/files-typescript/eslint.config.mjs.template +3 -0
- package/dist/generators/service/files-typescript/infra/service.ts.template +64 -0
- package/dist/generators/service/files-typescript/package.json.template +7 -0
- package/dist/generators/service/files-typescript/src/index.ts.template +2 -0
- package/dist/generators/service/files-typescript/src/main.ts.template +60 -0
- package/dist/generators/service/files-typescript/src/service.spec.ts.template +49 -0
- package/dist/generators/service/files-typescript/src/service.ts.template +57 -0
- package/dist/generators/service/files-typescript/tsconfig.app.json.template +12 -0
- package/dist/generators/service/files-typescript/tsconfig.json.template +7 -0
- package/dist/generators/service/files-typescript/tsconfig.spec.json.template +7 -0
- package/dist/generators/service/files-typescript/vitest.config.mts.template +17 -0
- package/dist/generators/service/schema.d.ts +8 -0
- package/dist/generators/service/schema.json +40 -0
- package/dist/generators/service/service.d.ts +5 -0
- package/dist/generators/service/service.d.ts.map +1 -0
- package/dist/generators/service/service.js +198 -0
- package/dist/generators/task/files-go/Dockerfile.template +10 -0
- package/dist/generators/task/files-go/README.md.template +11 -0
- package/dist/generators/task/files-go/cmd/main.go.template +22 -0
- package/dist/generators/task/files-go/docs/contracts.md.template +9 -0
- package/dist/generators/task/files-go/go.mod.template +3 -0
- package/dist/generators/task/files-python/Dockerfile.template +10 -0
- package/dist/generators/task/files-python/README.md.template +11 -0
- package/dist/generators/task/files-python/docs/contracts.md.template +9 -0
- package/dist/generators/task/files-python/pyproject.toml.template +12 -0
- package/dist/generators/task/files-python/src/main.py.template +26 -0
- package/dist/generators/task/files-rust/Cargo.toml.template +15 -0
- package/dist/generators/task/files-rust/Dockerfile.template +11 -0
- package/dist/generators/task/files-rust/README.md.template +11 -0
- package/dist/generators/task/files-rust/docs/contracts.md.template +9 -0
- package/dist/generators/task/files-rust/src/main.rs.template +12 -0
- package/dist/generators/task/files-typescript/Dockerfile.template +25 -0
- package/dist/generators/task/files-typescript/README.md.template +24 -0
- package/dist/generators/task/files-typescript/docs/architecture.md.template +18 -0
- package/dist/generators/task/files-typescript/docs/contracts.md.template +20 -0
- package/dist/generators/task/files-typescript/docs/operations.md.template +24 -0
- package/dist/generators/task/files-typescript/eslint.config.mjs.template +3 -0
- package/dist/generators/task/files-typescript/infra/task.ts.template +35 -0
- package/dist/generators/task/files-typescript/package.json.template +7 -0
- package/dist/generators/task/files-typescript/src/index.ts.template +6 -0
- package/dist/generators/task/files-typescript/src/main.ts.template +45 -0
- package/dist/generators/task/files-typescript/src/task.spec.ts.template +66 -0
- package/dist/generators/task/files-typescript/src/task.ts.template +71 -0
- package/dist/generators/task/files-typescript/tsconfig.app.json.template +13 -0
- package/dist/generators/task/files-typescript/tsconfig.json.template +19 -0
- package/dist/generators/task/files-typescript/tsconfig.spec.json.template +11 -0
- package/dist/generators/task/files-typescript/vitest.config.mts.template +9 -0
- package/dist/generators/task/schema.d.ts +8 -0
- package/dist/generators/task/schema.json +40 -0
- package/dist/generators/task/task.d.ts +5 -0
- package/dist/generators/task/task.d.ts.map +1 -0
- package/dist/generators/task/task.js +198 -0
- package/dist/generators.json +14 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/executors.json +3 -0
- package/generators.json +14 -0
- package/package.json +92 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @empire-builder-kit/containers
|
|
2
|
+
|
|
3
|
+
Empire Builder Kit container workload generators.
|
|
4
|
+
|
|
5
|
+
## Implemented Surface
|
|
6
|
+
|
|
7
|
+
- `@empire-builder-kit/containers:service`: scaffold a TypeScript-first backend service at `packages/<app>/apps/service`
|
|
8
|
+
- `@empire-builder-kit/containers:task`: scaffold a TypeScript-first backend task at `packages/<app>/apps/task`
|
|
9
|
+
|
|
10
|
+
## Current Direction
|
|
11
|
+
|
|
12
|
+
This milestone focuses on a mergeable TypeScript-first backend slice: a minimal HTTP service plus a batch-oriented task with shared container build conventions, Nx targets, Vitest coverage, app-local docs, and generated SST/ECS composition templates with a consistent local-dev command contract. Broader language presets can land in follow-on milestones.
|
|
13
|
+
|
|
14
|
+
## Building
|
|
15
|
+
|
|
16
|
+
Run `pnpm nx build @empire-builder-kit/containers` to build the library.
|
|
17
|
+
|
|
18
|
+
## Running unit tests
|
|
19
|
+
|
|
20
|
+
Run `pnpm nx test @empire-builder-kit/containers` to execute the unit tests via [Vitest](https://vitest.dev/).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FROM golang:1-bookworm AS builder
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
COPY go.mod go.sum* ./
|
|
4
|
+
RUN go mod download
|
|
5
|
+
COPY . .
|
|
6
|
+
RUN CGO_ENABLED=0 go build -o /service ./cmd/...
|
|
7
|
+
|
|
8
|
+
FROM gcr.io/distroless/static-debian12
|
|
9
|
+
COPY --from=builder /service /service
|
|
10
|
+
EXPOSE 3000
|
|
11
|
+
CMD ["/service"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# <%= projectTitle %>
|
|
2
|
+
|
|
3
|
+
Go service for the `<%= app %>` app group.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- `pnpm nx run <%= projectName %>:build` — compile binary
|
|
8
|
+
- `pnpm nx run <%= projectName %>:dev` — run locally
|
|
9
|
+
- `pnpm nx run <%= projectName %>:test` — run tests
|
|
10
|
+
- `pnpm nx run <%= projectName %>:lint` — run golangci-lint
|
|
11
|
+
- `pnpm nx run <%= projectName %>:docker-build` — build container image
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"log/slog"
|
|
6
|
+
"net/http"
|
|
7
|
+
"os"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type healthResponse struct {
|
|
11
|
+
Status string `json:"status"`
|
|
12
|
+
Service string `json:"service"`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func main() {
|
|
16
|
+
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
17
|
+
slog.SetDefault(logger)
|
|
18
|
+
|
|
19
|
+
mux := http.NewServeMux()
|
|
20
|
+
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
|
21
|
+
w.Header().Set("Content-Type", "application/json")
|
|
22
|
+
json.NewEncoder(w).Encode(healthResponse{
|
|
23
|
+
Status: "ok",
|
|
24
|
+
Service: "<%= projectName %>",
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
addr := ":3000"
|
|
29
|
+
slog.Info("listening", "addr", addr)
|
|
30
|
+
if err := http.ListenAndServe(addr, mux); err != nil {
|
|
31
|
+
slog.Error("server failed", "error", err)
|
|
32
|
+
os.Exit(1)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
FROM python:3.12-slim AS builder
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
COPY pyproject.toml ./
|
|
4
|
+
RUN pip install --no-cache-dir .
|
|
5
|
+
|
|
6
|
+
FROM python:3.12-slim
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
|
9
|
+
COPY --from=builder /usr/local/bin /usr/local/bin
|
|
10
|
+
COPY src/ src/
|
|
11
|
+
EXPOSE 3000
|
|
12
|
+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "3000"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# <%= projectTitle %>
|
|
2
|
+
|
|
3
|
+
Python (FastAPI) service for the `<%= app %>` app group.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- `pnpm nx run <%= projectName %>:dev` — run with auto-reload
|
|
8
|
+
- `pnpm nx run <%= projectName %>:test` — run pytest
|
|
9
|
+
- `pnpm nx run <%= projectName %>:lint` — run ruff
|
|
10
|
+
- `pnpm nx run <%= projectName %>:typecheck` — run mypy
|
|
11
|
+
- `pnpm nx run <%= projectName %>:docker-build` — build container image
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "<%= projectName %>"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
requires-python = ">=3.12"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"fastapi>=0.115",
|
|
7
|
+
"uvicorn[standard]>=0.34",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
[project.optional-dependencies]
|
|
11
|
+
dev = [
|
|
12
|
+
"pytest>=8",
|
|
13
|
+
"ruff>=0.8",
|
|
14
|
+
"mypy>=1.13",
|
|
15
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""<%= projectTitle %> — FastAPI service for the <%= app %> app group."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
|
|
8
|
+
logging.basicConfig(
|
|
9
|
+
stream=sys.stdout,
|
|
10
|
+
level=logging.INFO,
|
|
11
|
+
format="%(message)s",
|
|
12
|
+
)
|
|
13
|
+
logger = logging.getLogger("<%= projectName %>")
|
|
14
|
+
|
|
15
|
+
app = FastAPI(title="<%= projectTitle %>")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.get("/health")
|
|
19
|
+
async def health():
|
|
20
|
+
return {"status": "ok", "service": "<%= projectName %>"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "<%= projectName %>"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[[bin]]
|
|
7
|
+
name = "service"
|
|
8
|
+
path = "src/main.rs"
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
axum = "0.8"
|
|
12
|
+
serde = { version = "1", features = ["derive"] }
|
|
13
|
+
serde_json = "1"
|
|
14
|
+
tokio = { version = "1", features = ["full"] }
|
|
15
|
+
tracing = "0.1"
|
|
16
|
+
tracing-subscriber = { version = "0.3", features = ["json"] }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
FROM rust:1-bookworm AS builder
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
COPY Cargo.toml Cargo.lock* ./
|
|
4
|
+
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src
|
|
5
|
+
COPY src/ src/
|
|
6
|
+
RUN cargo build --release
|
|
7
|
+
|
|
8
|
+
FROM debian:bookworm-slim
|
|
9
|
+
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
|
10
|
+
COPY --from=builder /app/target/release/service /usr/local/bin/service
|
|
11
|
+
EXPOSE 3000
|
|
12
|
+
CMD ["service"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# <%= projectTitle %>
|
|
2
|
+
|
|
3
|
+
Rust service for the `<%= app %>` app group.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- `pnpm nx run <%= projectName %>:build` — compile release binary
|
|
8
|
+
- `pnpm nx run <%= projectName %>:dev` — run with cargo-watch
|
|
9
|
+
- `pnpm nx run <%= projectName %>:test` — run tests
|
|
10
|
+
- `pnpm nx run <%= projectName %>:lint` — run clippy
|
|
11
|
+
- `pnpm nx run <%= projectName %>:docker-build` — build container image
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
use axum::{routing::get, Json, Router};
|
|
2
|
+
use serde::Serialize;
|
|
3
|
+
use std::net::SocketAddr;
|
|
4
|
+
|
|
5
|
+
#[derive(Serialize)]
|
|
6
|
+
struct HealthResponse {
|
|
7
|
+
status: &'static str,
|
|
8
|
+
service: &'static str,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async fn health() -> Json<HealthResponse> {
|
|
12
|
+
Json(HealthResponse {
|
|
13
|
+
status: "ok",
|
|
14
|
+
service: "<%= projectName %>",
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[tokio::main]
|
|
19
|
+
async fn main() {
|
|
20
|
+
tracing_subscriber::fmt().json().init();
|
|
21
|
+
|
|
22
|
+
let app = Router::new().route("/health", get(health));
|
|
23
|
+
|
|
24
|
+
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
|
|
25
|
+
tracing::info!("listening on {}", addr);
|
|
26
|
+
|
|
27
|
+
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
|
28
|
+
axum::serve(listener, app).await.unwrap();
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1.7
|
|
2
|
+
|
|
3
|
+
FROM node:22-bookworm-slim AS build
|
|
4
|
+
WORKDIR /workspace
|
|
5
|
+
|
|
6
|
+
ENV CI=true
|
|
7
|
+
|
|
8
|
+
RUN corepack enable
|
|
9
|
+
|
|
10
|
+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml nx.json tsconfig.base.json tsconfig.json eslint.config.mjs ./
|
|
11
|
+
COPY packages ./packages
|
|
12
|
+
COPY tools ./tools
|
|
13
|
+
|
|
14
|
+
RUN pnpm install --frozen-lockfile
|
|
15
|
+
RUN pnpm nx run <%= projectName %>:build
|
|
16
|
+
|
|
17
|
+
FROM node:22-bookworm-slim AS runtime
|
|
18
|
+
WORKDIR /app
|
|
19
|
+
|
|
20
|
+
ENV NODE_ENV=production
|
|
21
|
+
ENV PORT=3000
|
|
22
|
+
ENV SERVICE_NAME=<%= className %>Service
|
|
23
|
+
|
|
24
|
+
COPY --from=build /workspace/dist/<%= projectRoot %>/ ./
|
|
25
|
+
|
|
26
|
+
EXPOSE 3000
|
|
27
|
+
|
|
28
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || '3000') + '/health').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"
|
|
29
|
+
|
|
30
|
+
CMD ["node", "main.js"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# <%= projectTitle %>
|
|
2
|
+
|
|
3
|
+
TypeScript-first backend container service for the `<%= appName %>` app group.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package provides a first mergeable ECS-style service slice with a health endpoint, structured log output, and container build conventions that fit the Empire Builder Kit app-group layout.
|
|
8
|
+
|
|
9
|
+
## Nx Targets
|
|
10
|
+
|
|
11
|
+
- `pnpm nx run <%= projectName %>:build`
|
|
12
|
+
- `pnpm nx run <%= projectName %>:lint`
|
|
13
|
+
- `pnpm nx run <%= projectName %>:typecheck`
|
|
14
|
+
- `pnpm nx run <%= projectName %>:test`
|
|
15
|
+
- `pnpm nx run <%= projectName %>:dev`
|
|
16
|
+
- `pnpm nx run <%= projectName %>:docker-build`
|
|
17
|
+
|
|
18
|
+
## Current Conventions
|
|
19
|
+
|
|
20
|
+
- keep long-running backend code in `src/`
|
|
21
|
+
- expose health behavior through `GET /health`
|
|
22
|
+
- prefer structured JSON logs so ECS and CloudWatch parsing stay predictable
|
|
23
|
+
- keep SST composition scaffolding in `infra/service.ts` so app-group infra can opt into the workload without rewriting the local-dev contract
|
|
24
|
+
- treat this package as app-group-internal until a supported SDK or API contract is published
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# <%= projectTitle %> Architecture
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
`<%= projectName %>` is the first TypeScript-first container workload slice for `<%= appName %>`. It gives the app group a long-running backend process under `packages/<%= appName %>/apps/service` without introducing language-preset sprawl in the first milestone.
|
|
6
|
+
|
|
7
|
+
## Runtime Shape
|
|
8
|
+
|
|
9
|
+
- `src/main.ts` starts a small Node HTTP server
|
|
10
|
+
- `src/service.ts` keeps config, health payloads, and structured log formatting testable
|
|
11
|
+
- `GET /health` is the default readiness endpoint for local and container checks
|
|
12
|
+
- `infra/service.ts` captures the recommended SST `Cluster` + `Service` composition seam, image path, service registry, and `sst dev` command contract
|
|
13
|
+
|
|
14
|
+
## Current Boundaries
|
|
15
|
+
|
|
16
|
+
- keep app-specific background processing and internal APIs here
|
|
17
|
+
- publish cross-app contracts through the owning app group's SDK instead of importing internals directly
|
|
18
|
+
- compose this workload into the owning app group's root `infra/` entrypoint instead of letting app code instantiate SST resources ad hoc
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# <%= projectTitle %> Contracts
|
|
2
|
+
|
|
3
|
+
## Local Development
|
|
4
|
+
|
|
5
|
+
- `pnpm nx run <%= projectName %>:dev` is the canonical local command for this workload.
|
|
6
|
+
- `infra/service.ts` passes the same command to SST via `dev.command` so `sst dev` and direct Nx usage stay aligned.
|
|
7
|
+
- `PORT`, `SERVICE_NAME`, and `SERVICE_VERSION` are the supported starter env overrides.
|
|
8
|
+
|
|
9
|
+
## Infra Composition
|
|
10
|
+
|
|
11
|
+
- import `register<%= className %>Service` from `apps/service/infra/service` into the owning app group's root `infra/` entrypoint
|
|
12
|
+
- provide an existing `sst.aws.Cluster` from app-group infrastructure instead of constructing a cluster inside runtime code
|
|
13
|
+
- use the generated `GET /health` path as the default ECS and load balancer health contract
|
|
14
|
+
- pass linked SST resources through `link` so runtime code can consume them through `Resource.*`
|
|
15
|
+
|
|
16
|
+
## Deployment Defaults
|
|
17
|
+
|
|
18
|
+
- image build context stays rooted at `packages/<%= appName %>/apps/service`
|
|
19
|
+
- local dev assumes `http://127.0.0.1:3000`
|
|
20
|
+
- load-balanced deployments default to forwarding `80/http` to `3000/http`
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# <%= projectTitle %> Operations
|
|
2
|
+
|
|
3
|
+
## Local Verification
|
|
4
|
+
|
|
5
|
+
- `pnpm nx run <%= projectName %>:build`
|
|
6
|
+
- `pnpm nx run <%= projectName %>:test`
|
|
7
|
+
- `pnpm nx run <%= projectName %>:dev`
|
|
8
|
+
- `node dist/<%= projectRoot %>/main.js`
|
|
9
|
+
|
|
10
|
+
Set `PORT`, `SERVICE_NAME`, or `SERVICE_VERSION` in the environment to override the defaults exposed by the starter service.
|
|
11
|
+
|
|
12
|
+
`<%= projectName %>:dev` uses `@swc-node/register` so the same command can be handed to `sst dev` from `infra/service.ts` without introducing a second local entrypoint contract.
|
|
13
|
+
|
|
14
|
+
## Container Workflow
|
|
15
|
+
|
|
16
|
+
- `pnpm nx run <%= projectName %>:docker-build`
|
|
17
|
+
- image build runs the workspace install plus `pnpm nx run <%= projectName %>:build`
|
|
18
|
+
- the container exposes `GET /health` and ships with a matching Docker `HEALTHCHECK`
|
|
19
|
+
- `infra/service.ts` wires the same health path into SST load balancer and ECS health checks for a mergeable default
|
|
20
|
+
|
|
21
|
+
## Next Steps
|
|
22
|
+
|
|
23
|
+
- replace the starter route handling with app-specific workload logic
|
|
24
|
+
- import `register<%= className %>Service` into the owning app group's `infra/` entrypoint when the workload is ready to deploy
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface Register<%= className %>ServiceArgs {
|
|
2
|
+
cluster: sst.aws.Cluster;
|
|
3
|
+
domain?: string | { name: string };
|
|
4
|
+
environment?: Record<string, string>;
|
|
5
|
+
link?: any[];
|
|
6
|
+
publicLoadBalancer?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function register<%= className %>Service({
|
|
10
|
+
cluster,
|
|
11
|
+
domain,
|
|
12
|
+
environment = {},
|
|
13
|
+
link = [],
|
|
14
|
+
publicLoadBalancer = false,
|
|
15
|
+
}: Register<%= className %>ServiceArgs) {
|
|
16
|
+
const service = new sst.aws.Service('<%= projectName %>', {
|
|
17
|
+
cluster,
|
|
18
|
+
image: {
|
|
19
|
+
context: './apps/service/<%= name %>',
|
|
20
|
+
dockerfile: 'Dockerfile',
|
|
21
|
+
},
|
|
22
|
+
dev: {
|
|
23
|
+
command: 'pnpm nx run <%= projectName %>:dev',
|
|
24
|
+
directory: '../../..',
|
|
25
|
+
url: 'http://127.0.0.1:3000',
|
|
26
|
+
},
|
|
27
|
+
environment: {
|
|
28
|
+
PORT: '3000',
|
|
29
|
+
SERVICE_NAME: '<%= className %>Service',
|
|
30
|
+
SERVICE_VERSION: '0.0.1',
|
|
31
|
+
...environment,
|
|
32
|
+
},
|
|
33
|
+
health: {
|
|
34
|
+
command: [
|
|
35
|
+
'CMD-SHELL',
|
|
36
|
+
'node -e "fetch(\'http://127.0.0.1:3000/health\').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"',
|
|
37
|
+
],
|
|
38
|
+
interval: '30 seconds',
|
|
39
|
+
timeout: '5 seconds',
|
|
40
|
+
retries: 3,
|
|
41
|
+
startPeriod: '10 seconds',
|
|
42
|
+
},
|
|
43
|
+
link,
|
|
44
|
+
logging: {
|
|
45
|
+
retention: '1 month',
|
|
46
|
+
},
|
|
47
|
+
serviceRegistry: {
|
|
48
|
+
port: 3000,
|
|
49
|
+
},
|
|
50
|
+
loadBalancer: {
|
|
51
|
+
public: publicLoadBalancer,
|
|
52
|
+
...(domain ? { domain } : {}),
|
|
53
|
+
rules: [{ listen: '80/http', forward: '3000/http' }],
|
|
54
|
+
health: {
|
|
55
|
+
'3000/http': {
|
|
56
|
+
path: '/health',
|
|
57
|
+
successCodes: '200-399',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return { service };
|
|
64
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { createLogger, createRequestContext } from '@empire-builder-kit/runtime';
|
|
3
|
+
import {
|
|
4
|
+
createHealthResponse,
|
|
5
|
+
readServiceConfig,
|
|
6
|
+
} from './service.js';
|
|
7
|
+
|
|
8
|
+
function readPort(env: NodeJS.ProcessEnv = process.env): number {
|
|
9
|
+
const value = env.PORT?.trim();
|
|
10
|
+
|
|
11
|
+
if (!value) {
|
|
12
|
+
return 3000;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parsed = Number.parseInt(value, 10);
|
|
16
|
+
|
|
17
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
18
|
+
throw new Error(`Invalid PORT value: ${value}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const config = readServiceConfig();
|
|
25
|
+
const port = readPort();
|
|
26
|
+
const logger = createLogger({ app: '<%= app %>', source: '<%= projectName %>' });
|
|
27
|
+
|
|
28
|
+
const server = createServer((request, response) => {
|
|
29
|
+
const method = request.method ?? 'GET';
|
|
30
|
+
const url = request.url ?? '/';
|
|
31
|
+
const ctx = createRequestContext({
|
|
32
|
+
app: '<%= app %>',
|
|
33
|
+
operation: `${method} ${url}`,
|
|
34
|
+
stage: process.env.SST_STAGE ?? process.env.EBK_STAGE ?? 'dev',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
logger.info('request received', {
|
|
38
|
+
correlationId: ctx.correlationId,
|
|
39
|
+
method,
|
|
40
|
+
path: url,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (method === 'GET' && url === '/health') {
|
|
44
|
+
response.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
|
|
45
|
+
response.end(JSON.stringify(createHealthResponse(config)));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
response.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
|
|
50
|
+
response.end(
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
error: 'Not Found',
|
|
53
|
+
path: url,
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
server.listen(port, () => {
|
|
59
|
+
logger.info('service listening', { port });
|
|
60
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createHealthResponse,
|
|
4
|
+
formatLogEvent,
|
|
5
|
+
readServiceConfig,
|
|
6
|
+
} from './service.js';
|
|
7
|
+
|
|
8
|
+
describe('service helpers', () => {
|
|
9
|
+
it('falls back to service defaults', () => {
|
|
10
|
+
expect(readServiceConfig({})).toEqual({
|
|
11
|
+
serviceName: '<%= className %>Service',
|
|
12
|
+
version: '0.0.1',
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('builds a stable health payload', () => {
|
|
17
|
+
expect(
|
|
18
|
+
createHealthResponse({
|
|
19
|
+
serviceName: '<%= className %>Service',
|
|
20
|
+
version: '1.2.3',
|
|
21
|
+
})
|
|
22
|
+
).toEqual({
|
|
23
|
+
service: '<%= className %>Service',
|
|
24
|
+
status: 'ok',
|
|
25
|
+
version: '1.2.3',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('formats structured log output', () => {
|
|
30
|
+
expect(
|
|
31
|
+
formatLogEvent(
|
|
32
|
+
{
|
|
33
|
+
serviceName: '<%= className %>Service',
|
|
34
|
+
version: '1.2.3',
|
|
35
|
+
},
|
|
36
|
+
'service.started',
|
|
37
|
+
{ port: 3000 }
|
|
38
|
+
)
|
|
39
|
+
).toBe(
|
|
40
|
+
JSON.stringify({
|
|
41
|
+
level: 'info',
|
|
42
|
+
message: 'service.started',
|
|
43
|
+
service: '<%= className %>Service',
|
|
44
|
+
version: '1.2.3',
|
|
45
|
+
port: 3000,
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface ServiceConfig {
|
|
2
|
+
serviceName: string;
|
|
3
|
+
version: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface HealthResponse {
|
|
7
|
+
service: string;
|
|
8
|
+
status: 'ok';
|
|
9
|
+
version: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LogContext {
|
|
13
|
+
[key: string]: string | number | boolean | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_SERVICE_NAME = '<%= className %>Service';
|
|
17
|
+
const DEFAULT_SERVICE_VERSION = '0.0.1';
|
|
18
|
+
|
|
19
|
+
function readOptionalEnv(
|
|
20
|
+
env: NodeJS.ProcessEnv,
|
|
21
|
+
key: string,
|
|
22
|
+
fallback: string
|
|
23
|
+
): string {
|
|
24
|
+
const value = env[key]?.trim();
|
|
25
|
+
return value && value.length > 0 ? value : fallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readServiceConfig(
|
|
29
|
+
env: NodeJS.ProcessEnv = process.env
|
|
30
|
+
): ServiceConfig {
|
|
31
|
+
return {
|
|
32
|
+
serviceName: readOptionalEnv(env, 'SERVICE_NAME', DEFAULT_SERVICE_NAME),
|
|
33
|
+
version: readOptionalEnv(env, 'SERVICE_VERSION', DEFAULT_SERVICE_VERSION),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createHealthResponse(config: ServiceConfig): HealthResponse {
|
|
38
|
+
return {
|
|
39
|
+
service: config.serviceName,
|
|
40
|
+
status: 'ok',
|
|
41
|
+
version: config.version,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatLogEvent(
|
|
46
|
+
config: ServiceConfig,
|
|
47
|
+
message: string,
|
|
48
|
+
context: LogContext = {}
|
|
49
|
+
): string {
|
|
50
|
+
return JSON.stringify({
|
|
51
|
+
level: 'info',
|
|
52
|
+
message,
|
|
53
|
+
service: config.serviceName,
|
|
54
|
+
version: config.version,
|
|
55
|
+
...context,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"outDir": "<%= offsetFromRoot %>dist/<%= projectRoot %>",
|
|
6
|
+
"declaration": false,
|
|
7
|
+
"declarationMap": false,
|
|
8
|
+
"emitDeclarationOnly": false
|
|
9
|
+
},
|
|
10
|
+
"include": ["src/**/*.ts"],
|
|
11
|
+
"exclude": ["src/**/*.spec.ts"]
|
|
12
|
+
}
|