一気にフォルダファイル作成
api/
├── Dockerfile
├── requirements.txt
├── chromadb.yaml #(※運用上は k8s/infra へ移動も検討可)
└── app/
├── main.py
├── config.py
├── db.py # SQLAlchemy接続・reflect
├── routers/
│ ├── portal_model.py # /model
│ ├── portal_field.py # /field
│ ├── portal_view.py # /view
│ ├── portal_smart_button.py# /smart_button
│ ├── portal_tab.py # /tab
│ ├── portal_menu.py # /menu
│ ├── translate.py # /translate (共通翻訳)
│ ├── chroma.py # /chroma (共通エクスポート/検索)
│ └── import.py # /import (共通インポート)
├── schemas/
│ ├── portal_model.py # Pydantic: PortalModel
│ ├── portal_field.py # PortalField
│ ├── portal_view.py # PortalView
│ ├── portal_smart_button.py# PortalSmartButton
│ ├── portal_tab.py # PortalTab
│ └── portal_menu.py # PortalMenu
└── services/
├── i18n.py # JA→EN 翻訳(非破壊・再帰)
├── embeddings.py
├── chroma_client.py
├── chroma_export.py # 各エンティティ→Chromaコレクション
├── portal_import.py # 共通CSV/JSON取込
├── view_export.py # ※単数形
└── view_import.py # ※単数形
k8s/
├─ base/
│ ├─ namespace.yaml
│ ├─ kustomization.yaml
│ ├─ postgres/
│ │ ├─ statefulset.yaml
│ │ ├─ service.yaml
│ │ ├─ init-configmap.yaml
│ │ └─ auth-secret.yaml # 実運用では overlays 側で生成推奨
│ ├─ odoo/
│ │ ├─ deployment.yaml
│ │ ├─ service.yaml
│ │ └─ ingress.yaml
│ ├─ portal-api/
│ │ ├─ deployment.yaml
│ │ ├─ service.yaml
│ │ ├─ ingress.yaml
│ │ ├─ hpa.yaml
│ │ ├─ api-configmap.yaml
│ │ └─ api-secret.yaml # 実運用では overlays 側で secretGenerator 推奨
│ ├─ chroma/
│ │ ├─ statefulset.yaml
│ │ └─ service.yaml
│ └─ cronjobs/
│ ├─ ir-src-rebuild.yaml
│ ├─ cj-extract-view.yaml
│ ├─ cj-extract-field.yaml
│ ├─ cj-translate-run.yaml
│ ├─ cj-chroma-package.yaml
│ └─ cj-chroma-upsert.yaml
└─ overlays/
├─ dev/
│ ├─ kustomization.yaml
│ ├─ patch-portal-api-image.yaml
│ ├─ patch-ingress-hosts.yaml
│ ├─ secret-generator.yaml
│ └─ configmap-generator.yaml
└─ prod/
├─ kustomization.yaml
├─ patch-portal-api-image.yaml
├─ patch-replicas-resources.yaml
├─ patch-ingress-hosts.yaml
├─ secret-generator.yaml
└─ configmap-generator.yaml
下記に古いもの
あなたの構成に合わせて、Kustomize前提の“ワンストップ”ブートストラップを1本のスクリプトにまとめました。
これを実行すると API / SQL ひな形+K8s(base と overlays/{dev,prod})一式が生成されます。
ポイント
- base は共通のみ(namespace/Secret/Configは含めない)
- overlay で namespace を作成し、secretGenerator/configMapGenerator で機密と環境値を差し替え
- prod は ALB注釈、HPA/IRSA/StorageClass(gp3)のパッチ雛形付き
- CronJob は
curlimages/curlを /bin/sh 経由で実行(環境変数展開OK)
#!/usr/bin/env bash
set -euo pipefail
ROOT="${1:-.}"
say() { printf "\033[1;32m==>\033[0m %s\n" "$*"; }
# =====================================================================================
# Dirs
# =====================================================================================
say "Creating directories..."
mkdir -p "$ROOT/api/app/routers" \
"$ROOT/api/app/schemas" \
"$ROOT/api/app/services" \
"$ROOT/api/app/repos" \
"$ROOT/api/app/utils" \
"$ROOT/api/app/openapi" \
"$ROOT/api/sql" \
"$ROOT/k8s/base/postgres" \
"$ROOT/k8s/base/odoo" \
"$ROOT/k8s/base/portal-api" \
"$ROOT/k8s/base/chroma" \
"$ROOT/k8s/base/cronjobs" \
"$ROOT/k8s/overlays/dev" \
"$ROOT/k8s/overlays/prod" \
"$ROOT/scripts"
# =====================================================================================
# API: Dockerfile & requirements
# =====================================================================================
say "Scaffolding API base..."
cat > "$ROOT/api/Dockerfile" <<'DOCKER'
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
COPY app /app/app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
DOCKER
cat > "$ROOT/api/requirements.txt" <<'REQ'
fastapi
uvicorn[standard]
pydantic
SQLAlchemy
psycopg2-binary
python-dotenv
requests
REQ
# =====================================================================================
# API: app skeleton
# =====================================================================================
cat > "$ROOT/api/app/__init__.py" <<'PY'
# app package
PY
cat > "$ROOT/api/app/main.py" <<'PY'
from fastapi import FastAPI
from .routers import extract, translate, writeback, package, chroma, status
app = FastAPI(title="Dev Portal API")
app.include_router(extract.router, prefix="/extract", tags=["extract"])
app.include_router(translate.router, prefix="/translate", tags=["translate"])
app.include_router(writeback.router, prefix="/writeback", tags=["writeback"])
app.include_router(package.router, prefix="/chroma", tags=["chroma-package"])
app.include_router(chroma.router, prefix="/chroma", tags=["chroma"])
app.include_router(status.router, prefix="/status", tags=["status"])
@app.get("/healthz")
def healthz(): return {"ok": True}
@app.get("/livez")
def livez(): return {"ok": True}
@app.get("/startupz")
def startupz(): return {"ok": True}
PY
cat > "$ROOT/api/app/config.py" <<'PY'
from pydantic import BaseSettings
class Settings(BaseSettings):
# Overridden by K8s ConfigMap/Secret in production
DATABASE_URL: str = "postgresql+psycopg2://portal:portal@localhost:5432/portal"
ODOO_DATABASE_URL: str = "postgresql+psycopg2://odoo:odoo@localhost:5432/odoo"
CHROMA_HOST: str = "localhost"
CHROMA_PORT: int = 8000
settings = Settings()
PY
cat > "$ROOT/api/app/db.py" <<'PY'
# TODO: SQLAlchemy engine/session
PY
cat > "$ROOT/api/app/routers/__init__.py" <<'PY'
from . import extract, translate, writeback, package, chroma, status
PY
cat > "$ROOT/api/app/routers/extract.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# POST /extract/view_common, POST /extract/field
PY
cat > "$ROOT/api/app/routers/translate.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# POST /translate/run, GET /translate
PY
cat > "$ROOT/api/app/routers/writeback.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# POST /writeback/view_common, POST /writeback/field
PY
cat > "$ROOT/api/app/routers/package.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# POST /chroma/package, GET /chroma/docs
PY
cat > "$ROOT/api/app/routers/chroma.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# POST /chroma/upsert
PY
cat > "$ROOT/api/app/routers/status.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# GET /status/summary, GET /samples/trace
PY
# Schemas
cat > "$ROOT/api/app/schemas/common.py" <<'PY'
from pydantic import BaseModel
from typing import Literal
Lang = Literal["ja", "en"]
Entity = Literal["field", "view_common"]
class NaturalKey(BaseModel):
entity: Entity
key: str
class Summary(BaseModel):
pending: int
translated: int
queued: int
upserted: int
PY
cat > "$ROOT/api/app/schemas/extract_view_common.py" <<'PY'
from pydantic import BaseModel
from typing import List, Literal
class ExtractViewCommonIn(BaseModel):
action_xmlids: List[str]
targets: List[Literal["ai_purpose","help"]] = ["ai_purpose","help"]
mode: Literal["upsert_if_changed","force_overwrite"] = "upsert_if_changed"
class ExtractViewCommonOut(BaseModel):
queued: int
skipped: int
PY
cat > "$ROOT/api/app/schemas/extract_field.py" <<'PY'
from pydantic import BaseModel
from typing import List
class ExtractFieldIn(BaseModel):
models: List[str] = []
fields: List[str] = []
mode: str = "upsert_if_changed"
class ExtractFieldOut(BaseModel):
queued: int
skipped: int
PY
cat > "$ROOT/api/app/schemas/translate_run.py" <<'PY'
from pydantic import BaseModel
from typing import List
from .common import Entity
class TranslateRunIn(BaseModel):
limit: int = 200
source_lang: str = "ja_JP"
target_lang: str = "en_US"
entities: List[Entity] = ["field","view_common"]
class TranslateRunOut(BaseModel):
processed: int
failed: int
PY
cat > "$ROOT/api/app/schemas/writeback_view_common.py" <<'PY'
from pydantic import BaseModel
class WritebackViewCommonIn(BaseModel):
mode: str = "skip_if_exists"
overwrite: bool = False
PY
cat > "$ROOT/api/app/schemas/writeback_field.py" <<'PY'
from pydantic import BaseModel
class WritebackFieldIn(BaseModel):
mode: str = "skip_if_exists"
overwrite: bool = False
PY
cat > "$ROOT/api/app/schemas/chroma_package.py" <<'PY'
from pydantic import BaseModel
from typing import Dict, List
from .common import Entity
class ChromaPackageIn(BaseModel):
entities: List[Entity] = ["field","view_common"]
lang: str = "ja"
collections: Dict[str,str] = {"field":"portal_field_ja","view_common":"portal_view_common_ja"}
limit: int = 500
class ChromaPackageOut(BaseModel):
queued: int
skipped: int
PY
cat > "$ROOT/api/app/schemas/chroma_upsert.py" <<'PY'
from pydantic import BaseModel
from typing import List
class ChromaUpsertIn(BaseModel):
collections: List[str] = ["portal_field_ja","portal_view_common_ja"]
limit: int = 1000
dry_run: bool = False
class ChromaUpsertOut(BaseModel):
upserted: int
failed: int
PY
cat > "$ROOT/api/app/schemas/status.py" <<'PY'
from pydantic import BaseModel
from .common import Summary
class StatusSummary(BaseModel):
summary: Summary
PY
# Services/repos/utils placeholders
cat > "$ROOT/api/app/services/extract.py" <<'PY'
# Orchestration for extract
PY
cat > "$ROOT/api/app/services/translate.py" <<'PY'
# Translation worker (pending -> translated)
PY
cat > "$ROOT/api/app/services/writeback.py" <<'PY'
# Optional writeback
PY
cat > "$ROOT/api/app/services/package.py" <<'PY'
# Make docs for Chroma export
PY
cat > "$ROOT/api/app/services/chroma_client.py" <<'PY'
# Chroma upsert/query
PY
cat > "$ROOT/api/app/services/natural_text.py" <<'PY'
# Document text generation templates
PY
for f in portal_translate_repo portal_chroma_doc_repo portal_view_common_repo portal_field_repo portal_model_repo; do
cat > "$ROOT/api/app/repos/${f}.py" <<PY
# repo placeholder: ${f}
PY
done
cat > "$ROOT/api/app/utils/natural_key.py" <<'PY'
# natural_key build/validate
PY
cat > "$ROOT/api/app/utils/lang_detect.py" <<'PY'
# JA text detection
PY
cat > "$ROOT/api/app/utils/html_strip.py" <<'PY'
# strip HTML
PY
cat > "$ROOT/api/app/utils/text_hash.py" <<'PY'
# text hashing for change detection
PY
cat > "$ROOT/api/app/utils/time.py" <<'PY'
# time utils
PY
cat > "$ROOT/api/app/openapi/openapi.yaml" <<'YAML'
openapi: 3.1.0
info: { title: Dev Portal API, version: 0.0.1 }
paths: {}
YAML
# SQL placeholders
for f in 000_extensions.sql 010_portal_core.sql 020_portal_translate.sql 030_portal_chroma_doc.sql; do
echo "-- $f (TBD)" > "$ROOT/api/sql/$f"
done
# =====================================================================================
# K8s: BASE (no namespace/secret/config in base)
# =====================================================================================
say "Scaffolding Kubernetes (base)..."
# base/kustomization.yaml
cat > "$ROOT/k8s/base/kustomization.yaml" <<'YAML'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- postgres/statefulset.yaml
- postgres/service.yaml
- postgres/init-configmap.yaml
- odoo/deployment.yaml
- odoo/service.yaml
- odoo/ingress.yaml
- odoo/pvc.yaml
- chroma/statefulset.yaml
- chroma/service.yaml
- portal-api/serviceaccount.yaml
- portal-api/deployment.yaml
- portal-api/service.yaml
- portal-api/ingress.yaml
- portal-api/hpa.yaml
- cronjobs/ir-src-rebuild.yaml
- cronjobs/cj-extract-view.yaml
- cronjobs/cj-extract-field.yaml
- cronjobs/cj-translate-run.yaml
- cronjobs/cj-chroma-package.yaml
- cronjobs/cj-chroma-upsert.yaml
YAML
# Postgres (expects Secret via overlay)
cat > "$ROOT/k8s/base/postgres/init-configmap.yaml" <<'YAML'
apiVersion: v1
kind: ConfigMap
metadata: { name: postgres-init }
data:
init-multi-db.sh: |
#!/usr/bin/env bash
set -euo pipefail
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-SQL
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname=current_setting('odoo.user', true)) THEN
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', current_setting('odoo.user', true), current_setting('odoo.pass', true));
END IF;
IF NOT EXISTS (SELECT FROM pg_database WHERE datname='odoo') THEN
EXECUTE format('CREATE DATABASE %I OWNER %I', 'odoo', current_setting('odoo.user', true));
END IF;
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname=current_setting('portal.user', true)) THEN
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', current_setting('portal.user', true), current_setting('portal.pass', true));
END IF;
IF NOT EXISTS (SELECT FROM pg_database WHERE datname='portal') THEN
EXECUTE format('CREATE DATABASE %I OWNER %I', 'portal', current_setting('portal.user', true));
END IF;
END
$$;
SQL
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" -d portal <<-SQL
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
SQL
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" -d odoo <<-SQL
CREATE EXTENSION IF NOT EXISTS pgcrypto;
SQL
YAML
cat > "$ROOT/k8s/base/postgres/statefulset.yaml" <<'YAML'
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: postgres }
spec:
serviceName: postgres
replicas: 1
selector: { matchLabels: { app: postgres } }
template:
metadata: { labels: { app: postgres } }
spec:
securityContext: { fsGroup: 999 }
containers:
- name: db
image: postgres:16
ports: [{ containerPort: 5432, name: pg }]
env:
- { name: POSTGRES_USER, value: postgres }
- { name: POSTGRES_PASSWORD, valueFrom: { secretKeyRef: { name: postgres-auth, key: POSTGRES_PASSWORD } } }
- { name: odoo.user, valueFrom: { secretKeyRef: { name: postgres-auth, key: ODOO_DB_USER } } }
- { name: odoo.pass, valueFrom: { secretKeyRef: { name: postgres-auth, key: ODOO_DB_PASSWORD } } }
- { name: portal.user, valueFrom: { secretKeyRef: { name: postgres-auth, key: PORTAL_DB_USER } } }
- { name: portal.pass, valueFrom: { secretKeyRef: { name: postgres-auth, key: PORTAL_DB_PASSWORD } } }
volumeMounts:
- { name: data, mountPath: /var/lib/postgresql/data }
- { name: init, mountPath: /docker-entrypoint-initdb.d }
volumes:
- name: init
configMap: { name: postgres-init, defaultMode: 0755 }
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 20Gi } }
YAML
cat > "$ROOT/k8s/base/postgres/service.yaml" <<'YAML'
apiVersion: v1
kind: Service
metadata: { name: postgres }
spec:
selector: { app: postgres }
ports: [{ name: pg, port: 5432, targetPort: 5432 }]
YAML
# Odoo
cat > "$ROOT/k8s/base/odoo/deployment.yaml" <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata: { name: odoo }
spec:
replicas: 1
selector: { matchLabels: { app: odoo } }
template:
metadata: { labels: { app: odoo } }
spec:
containers:
- name: odoo
image: odoo:18
ports: [{ containerPort: 8069, name: http }]
env:
- { name: HOST, value: "0.0.0.0" }
- { name: ODOO_DB_HOST, value: postgres }
- { name: ODOO_DB_PORT, value: "5432" }
- { name: ODOO_DB_USER, valueFrom: { secretKeyRef: { name: postgres-auth, key: ODOO_DB_USER } } }
- { name: ODOO_DB_PASSWORD, valueFrom: { secretKeyRef: { name: postgres-auth, key: ODOO_DB_PASSWORD } } }
- { name: ODOO_DB_NAME, value: "odoo" }
- { name: ODOO_PROXY_MODE, value: "true" }
volumeMounts:
- { name: filestore, mountPath: /var/lib/odoo }
readinessProbe: { httpGet: { path: /web, port: http }, initialDelaySeconds: 20, periodSeconds: 5 }
livenessProbe: { httpGet: { path: /web, port: http }, initialDelaySeconds: 60, periodSeconds: 10 }
volumes:
- name: filestore
persistentVolumeClaim: { claimName: odoo-filestore }
YAML
cat > "$ROOT/k8s/base/odoo/service.yaml" <<'YAML'
apiVersion: v1
kind: Service
metadata: { name: odoo }
spec:
selector: { app: odoo }
ports: [{ name: http, port: 80, targetPort: 8069 }]
YAML
cat > "$ROOT/k8s/base/odoo/ingress.yaml" <<'YAML'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: odoo
annotations: {}
spec:
rules:
- host: odoo.example.com
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: odoo, port: { number: 80 } } }
YAML
cat > "$ROOT/k8s/base/odoo/pvc.yaml" <<'YAML'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: odoo-filestore }
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 20Gi } }
YAML
# Chroma
cat > "$ROOT/k8s/base/chroma/statefulset.yaml" <<'YAML'
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: chroma }
spec:
serviceName: chroma
replicas: 1
selector: { matchLabels: { app: chroma } }
template:
metadata: { labels: { app: chroma } }
spec:
containers:
- name: chroma
image: ghcr.io/chroma-core/chroma:0.4.24
ports: [{ containerPort: 8000, name: http }]
env:
- { name: CHROMA_PORT, value: "8000" }
- { name: PERSIST_DIRECTORY, value: /chroma }
volumeMounts:
- { name: data, mountPath: /chroma }
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 20Gi } }
YAML
cat > "$ROOT/k8s/base/chroma/service.yaml" <<'YAML'
apiVersion: v1
kind: Service
metadata: { name: chroma }
spec:
selector: { app: chroma }
ports: [{ name: http, port: 8000, targetPort: 8000 }]
YAML
# Portal API (ServiceAccount for optional IRSA)
cat > "$ROOT/k8s/base/portal-api/serviceaccount.yaml" <<'YAML'
apiVersion: v1
kind: ServiceAccount
metadata: { name: portal-api }
YAML
cat > "$ROOT/k8s/base/portal-api/deployment.yaml" <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata: { name: portal-api }
spec:
selector: { matchLabels: { app: portal-api } }
template:
metadata: { labels: { app: portal-api } }
spec:
serviceAccountName: portal-api
containers:
- name: api
image: dev-portal-api:latest
ports: [{ containerPort: 8000, name: http }]
env:
- { name: APP_ENV, valueFrom: { configMapKeyRef: { name: api-config, key: APP_ENV } } }
- { name: LOG_LEVEL, valueFrom: { configMapKeyRef: { name: api-config, key: LOG_LEVEL } } }
- { name: API_PORT, valueFrom: { configMapKeyRef: { name: api-config, key: API_PORT } } }
- { name: DB_HOST, valueFrom: { configMapKeyRef: { name: api-config, key: DB_HOST } } }
- { name: DB_PORT, valueFrom: { configMapKeyRef: { name: api-config, key: DB_PORT } } }
- { name: DB_NAME, valueFrom: { configMapKeyRef: { name: api-config, key: DB_NAME } } }
- { name: CHROMA_HOST, valueFrom: { configMapKeyRef: { name: api-config, key: CHROMA_HOST } } }
- { name: CHROMA_PORT, valueFrom: { configMapKeyRef: { name: api-config, key: CHROMA_PORT } } }
- { name: CHROMA_COLLECTION_FIELD_JA, valueFrom: { configMapKeyRef: { name: api-config, key: CHROMA_COLLECTION_FIELD_JA } } }
- { name: CHROMA_COLLECTION_VIEW_JA, valueFrom: { configMapKeyRef: { name: api-config, key: CHROMA_COLLECTION_VIEW_JA } } }
- { name: DB_USER, valueFrom: { secretKeyRef: { name: api-auth, key: DB_USER } } }
- { name: DB_PASSWORD, valueFrom: { secretKeyRef: { name: api-auth, key: DB_PASSWORD } } }
- { name: API_BEARER_TOKEN, valueFrom: { secretKeyRef: { name: api-auth, key: API_BEARER_TOKEN } } }
readinessProbe: { httpGet: { path: /healthz, port: http }, initialDelaySeconds: 5, periodSeconds: 5 }
livenessProbe: { httpGet: { path: /livez, port: http }, initialDelaySeconds: 10, periodSeconds: 10 }
startupProbe: { httpGet: { path: /startupz,port: http }, failureThreshold: 30, periodSeconds: 2 }
YAML
cat > "$ROOT/k8s/base/portal-api/service.yaml" <<'YAML'
apiVersion: v1
kind: Service
metadata: { name: portal-api }
spec:
selector: { app: portal-api }
ports: [{ name: http, port: 80, targetPort: 8000 }]
YAML
cat > "$ROOT/k8s/base/portal-api/ingress.yaml" <<'YAML'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: portal-api
annotations: {}
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: portal-api, port: { number: 80 } } }
YAML
cat > "$ROOT/k8s/base/portal-api/hpa.yaml" <<'YAML'
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: portal-api }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: portal-api }
minReplicas: 2
maxReplicas: 8
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 60 } }
YAML
# CronJobs (use /bin/sh so $TOKEN expands)
cat > "$ROOT/k8s/base/cronjobs/ir-src-rebuild.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: ir-src-rebuild }
spec:
schedule: "0 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: psql
image: postgres:16
env:
- { name: DB_HOST, value: postgres }
- { name: DB_NAME, value: portal }
- { name: DB_USER, valueFrom: { secretKeyRef: { name: api-auth, key: DB_USER } } }
- { name: DB_PASSWORD, valueFrom: { secretKeyRef: { name: api-auth, key: DB_PASSWORD } } }
- { name: ODOO_DB_PASSWORD, valueFrom: { secretKeyRef: { name: postgres-auth, key: ODOO_DB_PASSWORD } } }
command: ["sh","-lc"]
args:
- |
export PGPASSWORD="$DB_PASSWORD";
psql "host=$DB_HOST dbname=$DB_NAME user=$DB_USER" -v ON_ERROR_STOP=1 <<'SQL'
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
-- TODO: ir_*_src rebuild SQL
SQL
YAML
cat > "$ROOT/k8s/base/cronjobs/cj-extract-view.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: cj-extract-view }
spec:
schedule: "*/30 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: curl
image: curlimages/curl:8.8.0
env:
- name: TOKEN
valueFrom: { secretKeyRef: { name: api-auth, key: API_BEARER_TOKEN } }
command: ["sh","-lc"]
args:
- >
curl -sS -X POST
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
--data '{"action_xmlids":[],"targets":["ai_purpose","help"],"mode":"upsert_if_changed"}'
http://portal-api.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local/extract/view_common
YAML
cat > "$ROOT/k8s/base/cronjobs/cj-extract-field.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: cj-extract-field }
spec:
schedule: "15 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: curl
image: curlimages/curl:8.8.0
env:
- name: TOKEN
valueFrom: { secretKeyRef: { name: api-auth, key: API_BEARER_TOKEN } }
command: ["sh","-lc"]
args:
- >
curl -sS -X POST
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
--data '{"models":[],"fields":[],"mode":"upsert_if_changed"}'
http://portal-api.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local/extract/field
YAML
cat > "$ROOT/k8s/base/cronjobs/cj-translate-run.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: cj-translate-run }
spec:
schedule: "*/20 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: curl
image: curlimages/curl:8.8.0
env:
- name: TOKEN
valueFrom: { secretKeyRef: { name: api-auth, key: API_BEARER_TOKEN } }
command: ["sh","-lc"]
args:
- >
curl -sS -X POST
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
--data '{"limit":200,"source_lang":"ja_JP","target_lang":"en_US","entities":["field","view_common"]}'
http://portal-api.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local/translate/run
YAML
cat > "$ROOT/k8s/base/cronjobs/cj-chroma-package.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: cj-chroma-package }
spec:
schedule: "*/30 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: curl
image: curlimages/curl:8.8.0
env:
- name: TOKEN
valueFrom: { secretKeyRef: { name: api-auth, key: API_BEARER_TOKEN } }
command: ["sh","-lc"]
args:
- >
curl -sS -X POST
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
--data '{"entities":["field","view_common"],"lang":"ja","collections":{"field":"portal_field_ja","view_common":"portal_view_common_ja"},"limit":500}'
http://portal-api.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local/chroma/package
YAML
cat > "$ROOT/k8s/base/cronjobs/cj-chroma-upsert.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: cj-chroma-upsert }
spec:
schedule: "*/15 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: curl
image: curlimages/curl:8.8.0
env:
- name: TOKEN
valueFrom: { secretKeyRef: { name: api-auth, key: API_BEARER_TOKEN } }
command: ["sh","-lc"]
args:
- >
curl -sS -X POST
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
--data '{"collections":["portal_field_ja","portal_view_common_ja"],"limit":1000,"dry_run":false}'
http://portal-api.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local/chroma/upsert
YAML
# =====================================================================================
# K8s: OVERLAYS (dev)
# =====================================================================================
say "Scaffolding overlays/dev..."
cat > "$ROOT/k8s/overlays/dev/namespace.yaml" <<'YAML'
apiVersion: v1
kind: Namespace
metadata: { name: portal-dev }
YAML
cat > "$ROOT/k8s/overlays/dev/kustomization.yaml" <<'YAML'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: portal-dev
resources:
- ../../base
- namespace.yaml
generatorOptions:
disableNameSuffixHash: true
# Replace config/secret from base expectations
configMapGenerator:
- name: api-config
behavior: replace
literals:
- APP_ENV=dev
- LOG_LEVEL=info
- API_PORT=8000
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=portal
- CHROMA_HOST=chroma
- CHROMA_PORT=8000
- CHROMA_COLLECTION_FIELD_JA=portal_field_ja
- CHROMA_COLLECTION_VIEW_JA=portal_view_common_ja
secretGenerator:
- name: api-auth
behavior: replace
literals:
- DB_USER=portal
- DB_PASSWORD=change-portal-pass
- API_BEARER_TOKEN=dev-token
- name: postgres-auth
behavior: replace
literals:
- POSTGRES_PASSWORD=change-super-pass
- ODOO_DB_USER=odoo
- ODOO_DB_PASSWORD=change-odoo-pass
- PORTAL_DB_USER=portal
- PORTAL_DB_PASSWORD=change-portal-pass
patchesStrategicMerge:
- patch-ingress-hosts.yaml
- patch-api-image.yaml
YAML
cat > "$ROOT/k8s/overlays/dev/patch-ingress-hosts.yaml" <<'YAML'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: odoo }
spec:
rules:
- host: odoo.dev.example.com
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: portal-api }
spec:
rules:
- host: api.dev.example.com
YAML
cat > "$ROOT/k8s/overlays/dev/patch-api-image.yaml" <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata: { name: portal-api }
spec:
template:
spec:
containers:
- name: api
image: your-registry/portal-api:dev
YAML
# =====================================================================================
# K8s: OVERLAYS (prod)
# =====================================================================================
say "Scaffolding overlays/prod..."
cat > "$ROOT/k8s/overlays/prod/namespace.yaml" <<'YAML'
apiVersion: v1
kind: Namespace
metadata: { name: portal-prod }
YAML
cat > "$ROOT/k8s/overlays/prod/kustomization.yaml" <<'YAML'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: portal-prod
resources:
- ../../base
- namespace.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: api-config
behavior: replace
literals:
- APP_ENV=prod
- LOG_LEVEL=info
- API_PORT=8000
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=portal
- CHROMA_HOST=chroma
- CHROMA_PORT=8000
- CHROMA_COLLECTION_FIELD_JA=portal_field_ja
- CHROMA_COLLECTION_VIEW_JA=portal_view_common_ja
secretGenerator:
- name: api-auth
behavior: replace
literals:
- DB_USER=portal
- DB_PASSWORD=***REDACTED***
- API_BEARER_TOKEN=***REDACTED***
- name: postgres-auth
behavior: replace
literals:
- POSTGRES_PASSWORD=***REDACTED***
- ODOO_DB_USER=odoo
- ODOO_DB_PASSWORD=***REDACTED***
- PORTAL_DB_USER=portal
- PORTAL_DB_PASSWORD=***REDACTED***
patchesStrategicMerge:
- patch-ingress-aws.yaml
- patch-hpa.yaml
- patch-storageclass.yaml
- patch-irsa.yaml
- patch-api-image.yaml
YAML
cat > "$ROOT/k8s/overlays/prod/patch-ingress-aws.yaml" <<'YAML'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: portal-api
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:REGION:ACCOUNT:certificate/XXXX
spec:
rules:
- host: api.example.com
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: odoo
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:REGION:ACCOUNT:certificate/YYYY
spec:
rules:
- host: odoo.example.com
YAML
cat > "$ROOT/k8s/overlays/prod/patch-hpa.yaml" <<'YAML'
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: portal-api }
spec:
minReplicas: 4
maxReplicas: 10
YAML
cat > "$ROOT/k8s/overlays/prod/patch-storageclass.yaml" <<'YAML'
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: postgres }
spec:
volumeClaimTemplates:
- metadata: { name: data }
spec:
storageClassName: gp3
resources: { requests: { storage: 100Gi } }
---
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: chroma }
spec:
volumeClaimTemplates:
- metadata: { name: data }
spec:
storageClassName: gp3
resources: { requests: { storage: 50Gi } }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: odoo-filestore }
spec:
storageClassName: gp3
resources: { requests: { storage: 100Gi } }
YAML
cat > "$ROOT/k8s/overlays/prod/patch-irsa.yaml" <<'YAML'
apiVersion: v1
kind: ServiceAccount
metadata:
name: portal-api
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/portal-api-irsa
YAML
cat > "$ROOT/k8s/overlays/prod/patch-api-image.yaml" <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata: { name: portal-api }
spec:
template:
spec:
containers:
- name: api
image: your-registry/portal-api:1.0.0
YAML
# =====================================================================================
# Helper scripts
# =====================================================================================
cat > "$ROOT/scripts/apply_dev.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
kubectl apply -k k8s/overlays/dev
SH
chmod +x "$ROOT/scripts/apply_dev.sh"
cat > "$ROOT/scripts/apply_prod.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
kubectl apply -k k8s/overlays/prod
SH
chmod +x "$ROOT/scripts/apply_prod.sh"
say "✅ Bootstrap complete."
echo
echo "Next steps:"
echo " 1) Build API image: docker build --no-cache -t your-registry/portal-api:dev -f api/Dockerfile api"
echo " 2) Push image (if needed) and then:"
echo " - Dev : ./scripts/apply_dev.sh"
echo " - Prod: ./scripts/apply_prod.sh (requires ALB Controller/EBS CSI/ACM ready)"
下記の**ワンコマンド(ブートストラップスクリプト)**を保存して実行すると、あなたが提示した API 構成(routers/schemas/services/repos/utils…)に加え、Kubernetes の Odoo / Postgres / API / Chroma / CronJob まで一式のディレクトリとプレースホルダーファイルを生成します。
使い方:
- 保存 →
chmod +x bootstrap_full.sh ./bootstrap_full.sh(カレント直下に生成)
もしくは./bootstrap_full.sh /path/to/project
#!/usr/bin/env bash
set -euo pipefail
ROOT="${1:-.}"
say() { printf "\033[1;32m==>\033[0m %s\n" "$*"; }
# ------------------------------------------------------------------------------
# Dirs
# ------------------------------------------------------------------------------
say "Creating directories..."
mkdir -p "$ROOT/api/app/routers" \
"$ROOT/api/app/schemas" \
"$ROOT/api/app/services" \
"$ROOT/api/app/repos" \
"$ROOT/api/app/utils" \
"$ROOT/api/app/openapi" \
"$ROOT/api/sql" \
"$ROOT/k8s/dev" \
"$ROOT/k8s/infra/postgres" \
"$ROOT/k8s/odoo" \
"$ROOT/k8s/cronjobs" \
"$ROOT/k8s/chroma" \
"$ROOT/scripts"
# ------------------------------------------------------------------------------
# Top-level: Dockerfile + requirements
# ------------------------------------------------------------------------------
say "Scaffolding API base..."
cat > "$ROOT/api/Dockerfile" <<'DOCKER'
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
COPY app /app/app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
DOCKER
cat > "$ROOT/api/requirements.txt" <<'REQ'
fastapi
uvicorn[standard]
pydantic
SQLAlchemy
psycopg2-binary
python-dotenv
requests
REQ
# ------------------------------------------------------------------------------
# app core
# ------------------------------------------------------------------------------
cat > "$ROOT/api/app/__init__.py" <<'PY'
# app package
PY
cat > "$ROOT/api/app/main.py" <<'PY'
from fastapi import FastAPI
from .routers import (
extract, translate, writeback, package, chroma, status
)
app = FastAPI(title="Dev Portal API")
# pipeline routers
app.include_router(extract.router, prefix="/extract", tags=["extract"])
app.include_router(translate.router, prefix="/translate", tags=["translate"])
app.include_router(writeback.router, prefix="/writeback", tags=["writeback"])
app.include_router(package.router, prefix="/chroma", tags=["chroma-package"])
app.include_router(chroma.router, prefix="/chroma", tags=["chroma"])
app.include_router(status.router, prefix="/status", tags=["status"])
@app.get("/healthz")
def healthz(): return {"ok": True}
@app.get("/livez")
def livez(): return {"ok": True}
@app.get("/startupz")
def startupz(): return {"ok": True}
PY
cat > "$ROOT/api/app/config.py" <<'PY'
from pydantic import BaseSettings
class Settings(BaseSettings):
# K8s の ConfigMap/Secret で上書きする前提のデフォルト
DATABASE_URL: str = "postgresql+psycopg2://portal:portal@localhost:5432/portal"
ODOO_DATABASE_URL: str = "postgresql+psycopg2://odoo:odoo@localhost:5432/odoo"
CHROMA_HOST: str = "localhost"
CHROMA_PORT: int = 8000
settings = Settings()
PY
cat > "$ROOT/api/app/db.py" <<'PY'
# TODO: SQLAlchemy engine/session をここに実装
PY
# ------------------------------------------------------------------------------
# routers
# ------------------------------------------------------------------------------
cat > "$ROOT/api/app/routers/__init__.py" <<'PY'
from . import extract, translate, writeback, package, chroma, status
PY
# ① 抽出
cat > "$ROOT/api/app/routers/extract.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# POST /extract/view_common, POST /extract/field をここに実装していきます
PY
# ② 翻訳
cat > "$ROOT/api/app/routers/translate.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# POST /translate/run, GET /translate をここに
PY
# ③ 書き戻し(任意)
cat > "$ROOT/api/app/routers/writeback.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# POST /writeback/view_common, POST /writeback/field をここに
PY
# ④ パッケージ
cat > "$ROOT/api/app/routers/package.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# POST /chroma/package, GET /chroma/docs をここに
PY
# ⑤ Chroma upsert
cat > "$ROOT/api/app/routers/chroma.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# POST /chroma/upsert をここに
PY
# ダッシュボード
cat > "$ROOT/api/app/routers/status.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# GET /status/summary, GET /samples/trace をここに
PY
# ------------------------------------------------------------------------------
# schemas
# ------------------------------------------------------------------------------
cat > "$ROOT/api/app/schemas/common.py" <<'PY'
from pydantic import BaseModel
from typing import Literal
Lang = Literal["ja", "en"]
Entity = Literal["field", "view_common"]
class NaturalKey(BaseModel):
entity: Entity
key: str # 例: field:{model}.{field_name} / view_common:{action_xmlid}
class Summary(BaseModel):
pending: int
translated: int
queued: int
upserted: int
PY
cat > "$ROOT/api/app/schemas/extract_view_common.py" <<'PY'
from pydantic import BaseModel
from typing import List, Literal
class ExtractViewCommonIn(BaseModel):
action_xmlids: List[str]
targets: List[Literal["ai_purpose","help"]] = ["ai_purpose","help"]
mode: Literal["upsert_if_changed","force_overwrite"] = "upsert_if_changed"
class ExtractViewCommonOut(BaseModel):
queued: int
skipped: int
PY
cat > "$ROOT/api/app/schemas/extract_field.py" <<'PY'
from pydantic import BaseModel
from typing import List, Optional
class ExtractFieldIn(BaseModel):
models: List[str] = []
fields: List[str] = []
mode: str = "upsert_if_changed"
class ExtractFieldOut(BaseModel):
queued: int
skipped: int
PY
cat > "$ROOT/api/app/schemas/translate_run.py" <<'PY'
from pydantic import BaseModel
from typing import List
from .common import Entity
class TranslateRunIn(BaseModel):
limit: int = 200
source_lang: str = "ja_JP"
target_lang: str = "en_US"
entities: List[Entity] = ["field","view_common"]
class TranslateRunOut(BaseModel):
processed: int
failed: int
PY
cat > "$ROOT/api/app/schemas/writeback_view_common.py" <<'PY'
from pydantic import BaseModel
class WritebackViewCommonIn(BaseModel):
mode: str = "skip_if_exists"
overwrite: bool = False
PY
cat > "$ROOT/api/app/schemas/writeback_field.py" <<'PY'
from pydantic import BaseModel
class WritebackFieldIn(BaseModel):
mode: str = "skip_if_exists"
overwrite: bool = False
PY
cat > "$ROOT/api/app/schemas/chroma_package.py" <<'PY'
from pydantic import BaseModel
from typing import Dict, List
from .common import Entity
class ChromaPackageIn(BaseModel):
entities: List[Entity] = ["field","view_common"]
lang: str = "ja"
collections: Dict[str,str] = {"field":"portal_field_ja","view_common":"portal_view_common_ja"}
limit: int = 500
class ChromaPackageOut(BaseModel):
queued: int
skipped: int
PY
cat > "$ROOT/api/app/schemas/chroma_upsert.py" <<'PY'
from pydantic import BaseModel
from typing import List
class ChromaUpsertIn(BaseModel):
collections: List[str] = ["portal_field_ja","portal_view_common_ja"]
limit: int = 1000
dry_run: bool = False
class ChromaUpsertOut(BaseModel):
upserted: int
failed: int
PY
cat > "$ROOT/api/app/schemas/status.py" <<'PY'
from pydantic import BaseModel
from .common import Summary
class StatusSummary(BaseModel):
summary: Summary
PY
# ------------------------------------------------------------------------------
# services
# ------------------------------------------------------------------------------
cat > "$ROOT/api/app/services/extract.py" <<'PY'
# 抽出のオーケストレーションを実装していきます
PY
cat > "$ROOT/api/app/services/translate.py" <<'PY'
# 翻訳ワーカー(pending→translated)
PY
cat > "$ROOT/api/app/services/writeback.py" <<'PY'
# 書き戻し(任意)
PY
cat > "$ROOT/api/app/services/package.py" <<'PY'
# translated→ready_for_chroma + chroma_doc作成
PY
cat > "$ROOT/api/app/services/chroma_client.py" <<'PY'
# Chroma接続(upsert/query)
PY
cat > "$ROOT/api/app/services/natural_text.py" <<'PY'
# doc_text生成(日本語テンプレ整形)
PY
# ------------------------------------------------------------------------------
# repos
# ------------------------------------------------------------------------------
for f in portal_translate_repo portal_chroma_doc_repo portal_view_common_repo portal_field_repo portal_model_repo; do
cat > "$ROOT/api/app/repos/${f}.py" <<PY
# repo placeholder: ${f}
PY
done
# ------------------------------------------------------------------------------
# utils
# ------------------------------------------------------------------------------
cat > "$ROOT/api/app/utils/natural_key.py" <<'PY'
# natural_key生成/検証(正規化・lower/trim)
PY
cat > "$ROOT/api/app/utils/lang_detect.py" <<'PY'
# 日本語判定(ぁ-ん/ァ-ン/一-龥)
PY
cat > "$ROOT/api/app/utils/html_strip.py" <<'PY'
# HTML除去(help_*_html→text)
PY
cat > "$ROOT/api/app/utils/text_hash.py" <<'PY'
# source_hash計算(差分判定)
PY
cat > "$ROOT/api/app/utils/time.py" <<'PY'
# ISO時刻などユーティリティ
PY
# ------------------------------------------------------------------------------
# openapi
# ------------------------------------------------------------------------------
cat > "$ROOT/api/app/openapi/openapi.yaml" <<'YAML'
openapi: 3.1.0
info: { title: Dev Portal API, version: 0.0.1 }
paths: {}
YAML
# ------------------------------------------------------------------------------
# SQL placeholders
# ------------------------------------------------------------------------------
for f in 000_extensions.sql 010_portal_core.sql 020_portal_translate.sql 030_portal_chroma_doc.sql; do
echo "-- $f (TBD)" > "$ROOT/api/sql/$f"
done
# ------------------------------------------------------------------------------
# Kubernetes: Namespaces
# ------------------------------------------------------------------------------
say "Scaffolding Kubernetes manifests..."
cat > "$ROOT/k8s/namespaces.yaml" <<'YAML'
apiVersion: v1
kind: Namespace
metadata: { name: portal-dev }
YAML
# ------------------------------------------------------------------------------
# Kubernetes: Postgres (shared instance: DB odoo & portal)
# ------------------------------------------------------------------------------
cat > "$ROOT/k8s/infra/postgres/auth-secret.yaml" <<'YAML'
apiVersion: v1
kind: Secret
metadata: { name: postgres-auth, namespace: portal-dev }
type: Opaque
stringData:
POSTGRES_PASSWORD: change-super-pass
ODOO_DB_USER: odoo
ODOO_DB_PASSWORD: change-odoo-pass
PORTAL_DB_USER: portal
PORTAL_DB_PASSWORD: change-portal-pass
YAML
cat > "$ROOT/k8s/infra/postgres/init-configmap.yaml" <<'YAML'
apiVersion: v1
kind: ConfigMap
metadata: { name: postgres-init, namespace: portal-dev }
data:
init-multi-db.sh: |
#!/usr/bin/env bash
set -euo pipefail
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-SQL
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname=current_setting('odoo.user', true)) THEN
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', current_setting('odoo.user', true), current_setting('odoo.pass', true));
END IF;
IF NOT EXISTS (SELECT FROM pg_database WHERE datname='odoo') THEN
EXECUTE format('CREATE DATABASE %I OWNER %I', 'odoo', current_setting('odoo.user', true));
END IF;
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname=current_setting('portal.user', true)) THEN
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', current_setting('portal.user', true), current_setting('portal.pass', true));
END IF;
IF NOT EXISTS (SELECT FROM pg_database WHERE datname='portal') THEN
EXECUTE format('CREATE DATABASE %I OWNER %I', 'portal', current_setting('portal.user', true));
END IF;
END
$$;
SQL
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" -d portal <<-SQL
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
SQL
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" -d odoo <<-SQL
CREATE EXTENSION IF NOT EXISTS pgcrypto;
SQL
YAML
cat > "$ROOT/k8s/infra/postgres/statefulset.yaml" <<'YAML'
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: postgres, namespace: portal-dev }
spec:
serviceName: postgres
replicas: 1
selector: { matchLabels: { app: postgres } }
template:
metadata: { labels: { app: postgres } }
spec:
securityContext: { fsGroup: 999 }
containers:
- name: db
image: postgres:16
ports: [{ containerPort: 5432, name: pg }]
env:
- { name: POSTGRES_USER, value: postgres }
- { name: POSTGRES_PASSWORD, valueFrom: { secretKeyRef: { name: postgres-auth, key: POSTGRES_PASSWORD } } }
- { name: odoo.user, valueFrom: { secretKeyRef: { name: postgres-auth, key: ODOO_DB_USER } } }
- { name: odoo.pass, valueFrom: { secretKeyRef: { name: postgres-auth, key: ODOO_DB_PASSWORD } } }
- { name: portal.user, valueFrom: { secretKeyRef: { name: postgres-auth, key: PORTAL_DB_USER } } }
- { name: portal.pass, valueFrom: { secretKeyRef: { name: postgres-auth, key: PORTAL_DB_PASSWORD } } }
volumeMounts:
- { name: data, mountPath: /var/lib/postgresql/data }
- { name: init, mountPath: /docker-entrypoint-initdb.d }
volumes:
- name: init
configMap: { name: postgres-init, defaultMode: 0755 }
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 20Gi } }
YAML
cat > "$ROOT/k8s/infra/postgres/service.yaml" <<'YAML'
apiVersion: v1
kind: Service
metadata: { name: postgres, namespace: portal-dev }
spec:
selector: { app: postgres }
ports: [{ name: pg, port: 5432, targetPort: 5432 }]
YAML
# ------------------------------------------------------------------------------
# Kubernetes: Odoo
# ------------------------------------------------------------------------------
cat > "$ROOT/k8s/odoo/odoo-configmap.yaml" <<'YAML'
apiVersion: v1
kind: ConfigMap
metadata: { name: odoo-config, namespace: portal-dev }
data:
ODOO_DB_NAME: odoo
ODOO_DB_HOST: postgres
ODOO_DB_PORT: "5432"
ODOO_PROXY_MODE: "true"
ODOO_DB_FILTER: "^odoo$"
ODOO_LOG_LEVEL: info
YAML
cat > "$ROOT/k8s/odoo/odoo-deployment.yaml" <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata: { name: odoo, namespace: portal-dev }
spec:
replicas: 1
selector: { matchLabels: { app: odoo } }
template:
metadata: { labels: { app: odoo } }
spec:
containers:
- name: odoo
image: odoo:16.0
ports: [{ containerPort: 8069, name: http }]
envFrom: [{ configMapRef: { name: odoo-config } }]
env:
- { name: ODOO_DB_USER, valueFrom: { secretKeyRef: { name: postgres-auth, key: ODOO_DB_USER } } }
- { name: ODOO_DB_PASSWORD, valueFrom: { secretKeyRef: { name: postgres-auth, key: ODOO_DB_PASSWORD } } }
volumeMounts:
- { name: filestore, mountPath: /var/lib/odoo }
readinessProbe: { httpGet: { path: /web, port: http }, initialDelaySeconds: 20, periodSeconds: 5 }
livenessProbe: { httpGet: { path: /web, port: http }, initialDelaySeconds: 60, periodSeconds: 10 }
volumes:
- name: filestore
persistentVolumeClaim: { claimName: odoo-filestore }
YAML
cat > "$ROOT/k8s/odoo/odoo-service.yaml" <<'YAML'
apiVersion: v1
kind: Service
metadata: { name: odoo, namespace: portal-dev }
spec:
selector: { app: odoo }
ports: [{ name: http, port: 80, targetPort: 8069 }]
YAML
cat > "$ROOT/k8s/odoo/odoo-ingress.yaml" <<'YAML'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: odoo
namespace: portal-dev
annotations: { kubernetes.io/ingress.class: nginx }
spec:
rules:
- host: odoo.dev.example.com
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: odoo, port: { number: 80 } } }
YAML
cat > "$ROOT/k8s/odoo/odoo-pvc.yaml" <<'YAML'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: odoo-filestore, namespace: portal-dev }
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 20Gi } }
YAML
# ------------------------------------------------------------------------------
# Kubernetes: API (Portal)
# ------------------------------------------------------------------------------
cat > "$ROOT/k8s/dev/api-configmap.yaml" <<'YAML'
apiVersion: v1
kind: ConfigMap
metadata: { name: api-config, namespace: portal-dev }
data:
APP_ENV: dev
LOG_LEVEL: info
API_PORT: "8000"
DB_HOST: postgres
DB_PORT: "5432"
DB_NAME: portal
CHROMA_HOST: chroma
CHROMA_PORT: "8000"
CHROMA_COLLECTION_FIELD_JA: portal_field_ja
CHROMA_COLLECTION_VIEW_JA: portal_view_common_ja
YAML
cat > "$ROOT/k8s/dev/api-secret.yaml" <<'YAML'
apiVersion: v1
kind: Secret
metadata: { name: api-auth, namespace: portal-dev }
type: Opaque
stringData:
DB_USER: portal
DB_PASSWORD: change-portal-pass
API_BEARER_TOKEN: dev-token
YAML
cat > "$ROOT/k8s/dev/api-deployment.yaml" <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata: { name: portal-api, namespace: portal-dev }
spec:
replicas: 2
selector: { matchLabels: { app: portal-api } }
template:
metadata: { labels: { app: portal-api } }
spec:
containers:
- name: api
image: dev-portal-api:latest
ports: [{ containerPort: 8000, name: http }]
envFrom:
- { configMapRef: { name: api-config } }
- { secretRef: { name: api-auth } }
readinessProbe: { httpGet: { path: /healthz, port: http }, initialDelaySeconds: 5, periodSeconds: 5 }
livenessProbe: { httpGet: { path: /livez, port: http }, initialDelaySeconds: 10, periodSeconds: 10 }
startupProbe: { httpGet: { path: /startupz,port: http }, failureThreshold: 30, periodSeconds: 2 }
YAML
cat > "$ROOT/k8s/dev/api-service.yaml" <<'YAML'
apiVersion: v1
kind: Service
metadata: { name: portal-api, namespace: portal-dev }
spec:
selector: { app: portal-api }
ports: [{ name: http, port: 80, targetPort: 8000 }]
YAML
cat > "$ROOT/k8s/dev/ingress-api.yaml" <<'YAML'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: portal-api
namespace: portal-dev
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: api.dev.example.com
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: portal-api, port: { number: 80 } } }
YAML
cat > "$ROOT/k8s/dev/hpa-api.yaml" <<'YAML'
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: portal-api, namespace: portal-dev }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: portal-api }
minReplicas: 2
maxReplicas: 8
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 60 } }
YAML
# ------------------------------------------------------------------------------
# Kubernetes: Chroma (dev)
# ------------------------------------------------------------------------------
cat > "$ROOT/k8s/chroma/statefulset.yaml" <<'YAML'
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: chroma, namespace: portal-dev }
spec:
serviceName: chroma
replicas: 1
selector: { matchLabels: { app: chroma } }
template:
metadata: { labels: { app: chroma } }
spec:
containers:
- name: chroma
image: chromadb/chroma:latest
ports: [{ containerPort: 8000, name: http }]
env:
- { name: CHROMA_SERVER_HOST, value: "0.0.0.0" }
- { name: CHROMA_SERVER_HTTP_PORT, value: "8000" }
volumeMounts:
- { name: data, mountPath: /chroma/chroma }
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 20Gi } }
YAML
cat > "$ROOT/k8s/chroma/service.yaml" <<'YAML'
apiVersion: v1
kind: Service
metadata: { name: chroma, namespace: portal-dev }
spec:
selector: { app: chroma }
ports: [{ name: http, port: 8000, targetPort: 8000 }]
YAML
# ------------------------------------------------------------------------------
# Kubernetes: DB migration Job (apply DDL)
# ------------------------------------------------------------------------------
cat > "$ROOT/k8s/dev/portal-db-migrate-job.yaml" <<'YAML'
apiVersion: batch/v1
kind: Job
metadata: { name: portal-db-migrate, namespace: portal-dev }
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: migrate
image: dev-portal-api:latest
envFrom:
- { configMapRef: { name: api-config } }
- { secretRef: { name: api-auth } }
command: ["sh", "-c"]
args:
- >
python -m scripts.apply_sql /app/sql/000_extensions.sql &&
python -m scripts.apply_sql /app/sql/010_portal_core.sql &&
python -m scripts.apply_sql /app/sql/020_portal_translate.sql &&
python -m scripts.apply_sql /app/sql/030_portal_chroma_doc.sql
YAML
# ------------------------------------------------------------------------------
# Kubernetes: CronJobs (ir_*_src rebuild & pipeline)
# ------------------------------------------------------------------------------
cat > "$ROOT/k8s/cronjobs/ir-src-rebuild.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: ir-src-rebuild, namespace: portal-dev }
spec:
schedule: "0 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: psql
image: postgres:16
env:
- { name: DB_HOST, value: postgres }
- { name: DB_NAME, value: portal }
- { name: DB_USER, valueFrom: { secretKeyRef: { name: api-auth, key: DB_USER } } }
- { name: DB_PASSWORD, valueFrom: { secretKeyRef: { name: api-auth, key: DB_PASSWORD } } }
- { name: ODOO_DB_PASSWORD, valueFrom: { secretKeyRef: { name: postgres-auth, key: ODOO_DB_PASSWORD } } }
command: ["bash", "-lc"]
args:
- |
export PGPASSWORD="$DB_PASSWORD";
psql "host=$DB_HOST dbname=$DB_NAME user=$DB_USER" -v ON_ERROR_STOP=1 <<'SQL'
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_foreign_server WHERE srvname='odoo_server') THEN
CREATE SERVER odoo_server FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'postgres', dbname 'odoo', port '5432');
END IF;
END$$;
CREATE SCHEMA IF NOT EXISTS odoo_fdw;
CREATE USER MAPPING IF NOT EXISTS FOR CURRENT_USER SERVER odoo_server
OPTIONS (user 'odoo', password :'odoo_pass');
-- ここに action-centric の ir_*_src 再構築SQLを貼り付けてください
SQL
envFrom:
- secretRef:
name: postgres-auth
env:
- name: odoo_pass
valueFrom: { secretKeyRef: { name: postgres-auth, key: ODOO_DB_PASSWORD } }
YAML
cat > "$ROOT/k8s/cronjobs/cj-extract-view.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: cj-extract-view, namespace: portal-dev }
spec:
schedule: "*/30 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: curl
image: curlimages/curl:8.8.0
env:
- name: TOKEN
valueFrom: { secretKeyRef: { name: api-auth, key: API_BEARER_TOKEN } }
args:
- >
-sS -X POST
-H "Authorization: Bearer $(TOKEN)"
-H "Content-Type: application/json"
--data '{"action_xmlids":[],"targets":["ai_purpose","help"],"mode":"upsert_if_changed"}'
http://portal-api.portal-dev.svc.cluster.local/extract/view_common
YAML
cat > "$ROOT/k8s/cronjobs/cj-extract-field.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: cj-extract-field, namespace: portal-dev }
spec:
schedule: "15 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: curl
image: curlimages/curl:8.8.0
env:
- name: TOKEN
valueFrom: { secretKeyRef: { name: api-auth, key: API_BEARER_TOKEN } }
args:
- >
-sS -X POST
-H "Authorization: Bearer $(TOKEN)"
-H "Content-Type: application/json"
--data '{"models":[],"fields":[],"mode":"upsert_if_changed"}'
http://portal-api.portal-dev.svc.cluster.local/extract/field
YAML
cat > "$ROOT/k8s/cronjobs/cj-translate-run.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: cj-translate-run, namespace: portal-dev }
spec:
schedule: "*/20 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: curl
image: curlimages/curl:8.8.0
env:
- name: TOKEN
valueFrom: { secretKeyRef: { name: api-auth, key: API_BEARER_TOKEN } }
args:
- >
-sS -X POST
-H "Authorization: Bearer $(TOKEN)"
-H "Content-Type: application/json"
--data '{"limit":200,"source_lang":"ja_JP","target_lang":"en_US","entities":["field","view_common"]}'
http://portal-api.portal-dev.svc.cluster.local/translate/run
YAML
cat > "$ROOT/k8s/cronjobs/cj-chroma-package.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: cj-chroma-package, namespace: portal-dev }
spec:
schedule: "*/30 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: curl
image: curlimages/curl:8.8.0
env:
- name: TOKEN
valueFrom: { secretKeyRef: { name: api-auth, key: API_BEARER_TOKEN } }
args:
- >
-sS -X POST
-H "Authorization: Bearer $(TOKEN)"
-H "Content-Type: application/json"
--data '{"entities":["field","view_common"],"lang":"ja","collections":{"field":"portal_field_ja","view_common":"portal_view_common_ja"},"limit":500}'
http://portal-api.portal-dev.svc.cluster.local/chroma/package
YAML
cat > "$ROOT/k8s/cronjobs/cj-chroma-upsert.yaml" <<'YAML'
apiVersion: batch/v1
kind: CronJob
metadata: { name: cj-chroma-upsert, namespace: portal-dev }
spec:
schedule: "*/15 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: curl
image: curlimages/curl:8.8.0
env:
- name: TOKEN
valueFrom: { secretKeyRef: { name: api-auth, key: API_BEARER_TOKEN } }
args:
- >
-sS -X POST
-H "Authorization: Bearer $(TOKEN)"
-H "Content-Type: application/json"
--data '{"collections":["portal_field_ja","portal_view_common_ja"],"limit":1000,"dry_run":false}'
http://portal-api.portal-dev.svc.cluster.local/chroma/upsert
YAML
# ------------------------------------------------------------------------------
# Helper scripts
# ------------------------------------------------------------------------------
cat > "$ROOT/scripts/m1_apply.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
kubectl apply -f k8s/namespaces.yaml
kubectl -n portal-dev apply -f k8s/infra/postgres/
kubectl -n portal-dev apply -f k8s/odoo/
kubectl -n portal-dev apply -f k8s/chroma/
kubectl -n portal-dev apply -f k8s/dev/
kubectl -n portal-dev apply -f k8s/cronjobs/
SH
chmod +x "$ROOT/scripts/m1_apply.sh"
cat > "$ROOT/scripts/m1_delete.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
kubectl -n portal-dev delete -f k8s/cronjobs/ --ignore-not-found
kubectl -n portal-dev delete -f k8s/dev/ --ignore-not-found
kubectl -n portal-dev delete -f k8s/chroma/ --ignore-not-found
kubectl -n portal-dev delete -f k8s/odoo/ --ignore-not-found
kubectl -n portal-dev delete -f k8s/infra/postgres/ --ignore-not-found
kubectl delete -f k8s/namespaces.yaml --ignore-not-found
SH
chmod +x "$ROOT/scripts/m1_delete.sh"
say "✅ Bootstrap complete."
cat > bootstrap_portal_tree.sh <<'BASH'
#!/usr/bin/env bash
set -euo pipefail
ROOT="${1:-.}"
say() { printf "\033[1;32m==>\033[0m %s\n" "$*"; }
# --- Dirs ------------------------------------------------------------------
say "Creating directories..."
mkdir -p "$ROOT/api/app/routers" \
"$ROOT/api/app/schemas" \
"$ROOT/api/app/services" \
"$ROOT/k8s/dev" \
"$ROOT/k8s/infra/postgres" \
"$ROOT/k8s/odoo" \
"$ROOT/scripts"
# --- Top-level files -------------------------------------------------------
say "Creating top-level files..."
cat > "$ROOT/api/Dockerfile" <<'DOCKER'
# minimal FastAPI image
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
COPY app /app/app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
DOCKER
cat > "$ROOT/api/requirements.txt" <<'REQ'
fastapi
uvicorn[standard]
pydantic
SQLAlchemy
psycopg2-binary
python-dotenv
REQ
# optional placeholder
: > "$ROOT/api/chromadb.yaml"
# --- app: core files -------------------------------------------------------
say "Scaffolding app core..."
cat > "$ROOT/api/app/__init__.py" <<'PY'
# app package
PY
cat > "$ROOT/api/app/main.py" <<'PY'
from fastapi import FastAPI
from .routers import import as import_router
app = FastAPI(title="Dev Portal API")
# mount minimal router(s)
app.include_router(import_router.router, prefix="", tags=["import"])
@app.get("/health")
def health():
return {"ok": True}
PY
cat > "$ROOT/api/app/config.py" <<'PY'
from pydantic import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str = "postgresql+psycopg2://dev:dev@localhost:5432/devportal"
ODOO_DATABASE_URL: str = "postgresql+psycopg2://odoo:odoo@localhost:5432/odoo"
settings = Settings()
PY
cat > "$ROOT/api/app/db.py" <<'PY'
# optional: add SQLAlchemy engine/session here later if必要
PY
# --- routers (単数形) ------------------------------------------------------
say "Creating routers..."
cat > "$ROOT/api/app/routers/__init__.py" <<'PY'
from . import import as import # noqa: F401
PY
# import only (UIで選択→UPSERT) の入口
cat > "$ROOT/api/app/routers/import.py" <<'PY'
from fastapi import APIRouter
router = APIRouter()
# ここに /import/odoo/field /import/odoo/view などのエンドポイントを追加していきます
PY
# プレースホルダ(必要になった時に中身を実装)
for f in portal_model portal_field portal_view portal_smart_button portal_tab portal_menu; do
cat > "$ROOT/api/app/routers/${f}.py" <<'PY'
# router placeholder (add endpoints later)
from fastapi import APIRouter
router = APIRouter()
PY
done
# --- schemas (単数形) ------------------------------------------------------
say "Creating schemas..."
for f in portal_model portal_field portal_view portal_smart_button portal_tab portal_menu; do
cat > "$ROOT/api/app/schemas/${f}.py" <<'PY'
# pydantic schemas placeholder
from pydantic import BaseModel
class Placeholder(BaseModel):
id: int | None = None
PY
done
# --- services(今回は取り込み専用の最小2本) -------------------------------
say "Creating services..."
cat > "$ROOT/api/app/services/odoo_intake.py" <<'PY'
# Odoo標準→ir_*_src を参照して取り込み元を準備する層(後で実装)
PY
cat > "$ROOT/api/app/services/odoo_upsert.py" <<'PY'
# UI選択分を portal_* へUPSERTする層(後で実装)
PY
# --- k8s: dev --------------------------------------------------------------
say "Creating k8s placeholders..."
cat > "$ROOT/k8s/dev/api-configmap.yaml" <<'YAML'
apiVersion: v1
kind: ConfigMap
metadata:
name: dev-portal-api-config
data: {}
YAML
cat > "$ROOT/k8s/dev/api-deployment.yaml" <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata:
name: dev-portal-api
spec:
replicas: 1
selector: { matchLabels: { app: dev-portal-api } }
template:
metadata: { labels: { app: dev-portal-api } }
spec:
containers:
- name: api
image: dev-portal-api:latest
ports: [{ containerPort: 8000 }]
YAML
cat > "$ROOT/k8s/dev/api-service.yaml" <<'YAML'
apiVersion: v1
kind: Service
metadata: { name: dev-portal-api }
spec:
selector: { app: dev-portal-api }
ports:
- port: 80
targetPort: 8000
YAML
cat > "$ROOT/k8s/dev/ingress-api.yaml" <<'YAML'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: dev-portal-api }
spec:
rules: []
YAML
cat > "$ROOT/k8s/dev/portal_fields-schema-configmap.yaml" <<'YAML'
apiVersion: v1
kind: ConfigMap
metadata:
name: portal-fields-schema
data: {}
YAML
# --- k8s: infra/postgres ---------------------------------------------------
cat > "$ROOT/k8s/infra/postgres/devportal-db-init-job.yaml" <<'YAML'
apiVersion: batch/v1
kind: Job
metadata: { name: devportal-db-init }
spec: { template: { spec: { restartPolicy: Never } } }
YAML
cat > "$ROOT/k8s/infra/postgres/odoo-db-init-job.yaml" <<'YAML'
apiVersion: batch/v1
kind: Job
metadata: { name: odoo-db-init }
spec: { template: { spec: { restartPolicy: Never } } }
YAML
cat > "$ROOT/k8s/infra/postgres/pg-smoke-job.yaml" <<'YAML'
apiVersion: batch/v1
kind: Job
metadata: { name: pg-smoke }
spec: { template: { spec: { restartPolicy: Never } } }
YAML
cat > "$ROOT/k8s/infra/postgres/service.yaml" <<'YAML'
apiVersion: v1
kind: Service
metadata: { name: postgres }
spec: { ports: [{ port: 5432 }], selector: { app: postgres } }
YAML
cat > "$ROOT/k8s/infra/postgres/statefulset.yaml" <<'YAML'
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: postgres }
spec:
serviceName: postgres
selector: { matchLabels: { app: postgres } }
template:
metadata: { labels: { app: postgres } }
spec: { containers: [{ name: postgres, image: postgres:16, ports: [{ containerPort: 5432 }] }] }
YAML
# --- k8s: namespaces & odoo ------------------------------------------------
cat > "$ROOT/k8s/namespaces.yaml" <<'YAML'
apiVersion: v1
kind: Namespace
metadata: { name: dev }
---
apiVersion: v1
kind: Namespace
metadata: { name: infra }
YAML
cat > "$ROOT/k8s/odoo/ingress-odoo.yaml" <<'YAML'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: odoo }
spec: { rules: [] }
YAML
cat > "$ROOT/k8s/odoo/odoo-configmap.yaml" <<'YAML'
apiVersion: v1
kind: ConfigMap
metadata: { name: odoo-config }
data: {}
YAML
cat > "$ROOT/k8s/odoo/odoo-deployment.yaml" <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata: { name: odoo }
spec:
replicas: 1
selector: { matchLabels: { app: odoo } }
template:
metadata: { labels: { app: odoo } }
spec: { containers: [{ name: odoo, image: odoo:17, ports: [{ containerPort: 8069 }] }] }
YAML
cat > "$ROOT/k8s/odoo/odoo-init-job.yaml" <<'YAML'
apiVersion: batch/v1
kind: Job
metadata: { name: odoo-init }
spec: { template: { spec: { restartPolicy: Never } } }
YAML
cat > "$ROOT/k8s/odoo/odoo-pvc.yaml" <<'YAML'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: odoo-data }
spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: 5Gi } } }
YAML
cat > "$ROOT/k8s/odoo/odoo-service.yaml" <<'YAML'
apiVersion: v1
kind: Service
metadata: { name: odoo }
spec: { ports: [{ port: 8069 }], selector: { app: odoo } }
YAML
# --- scripts ---------------------------------------------------------------
say "Creating helper scripts..."
cat > "$ROOT/scripts/m1_apply.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
kubectl apply -f k8s/namespaces.yaml
kubectl apply -f k8s/infra/postgres/
kubectl apply -f k8s/dev/
kubectl apply -f k8s/odoo/
SH
chmod +x "$ROOT/scripts/m1_apply.sh"
cat > "$ROOT/scripts/m1_delete.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
kubectl delete -f k8s/odoo/ --ignore-not-found
kubectl delete -f k8s/dev/ --ignore-not-found
kubectl delete -f k8s/infra/postgres/ --ignore-not-found
kubectl delete -f k8s/namespaces.yaml --ignore-not-found
SH
chmod +x "$ROOT/scripts/m1_delete.sh"
say "Done. Tree bootstrapped."
BASH
chmod +x bootstrap_portal_tree.sh
./bootstrap_portal_tree.sh
了解!作った各ファイルの“役割ひとこと解説”と、前回版をそのまま流用できるかを一目でわかるようにまとめます。
(パスはリポジトリ直下基準)
役割ひとこと&流用可否
| パス | 役割(何をするか) | 前回そのまま流用可? | メモ(パスワード/設定系) |
|---|---|---|---|
api/Dockerfile | API用コンテナのビルド定義 | ○ 推奨 | 既存のイメージ/レイヤ/apt追加があるなら前回のをそのまま使うのが安全。秘密情報は書かない。 |
api/requirements.txt | Python依存 | ○ 推奨 | バージョン差で動作が変わるため、前回のままが無難。 |
api/chromadb.yaml | Chromaの設定(現フェーズでは未使用可) | ○ | 触らず可。後段の埋め込み/検索で再利用。 |
api/app/__init__.py | パッケージマーカー | どちらでも | 空でも可。 |
api/app/main.py | FastAPIエントリ。ルーターをマウント | △ | 既存のmain.pyがあるなら流用でOK。新ルーター名(routers/import.py 等)だけ合わせればOK。 |
api/app/config.py | 環境変数読み込み | ○(流用推奨) | パスワードは.env/K8s Secretから。既存の環境変数名を変えたくない場合、前回のconfig.pyをそのまま使うのが最短。 |
api/app/db.py | DB接続/Session管理 | ○ | 既存の接続初期化(SQLAlchemy 等)をそのまま。新テーブル追加でも接続設定は不変。 |
api/app/routers/import.py | UIで選んだ ir_*_src を取り込みUPSERT | 新規 | 前回はimports.pyだったなら、残してもOK。新仕様は単数形に寄せる想定。 |
api/app/routers/portal_*.py | 各エンティティ用のAPI口(単数形、今は空) | 新規 | 必要になったら中身実装。前回のfields.py,menus.py等は残して共存でも可。 |
api/app/schemas/portal_*.py | Pydanticスキーマの置き場(今は空) | 新規 | 後で定義追加。 |
api/app/services/odoo_intake.py | 参照用 ir_*_src を読む層 | 新規 | 前回のfile_import.py系は不要。 |
api/app/services/odoo_upsert.py | portal_* へUPSERTする層 | 新規 | 前回のchroma_*やembeddings.pyは現フェーズ未使用でOK。 |
k8s/**(dev/infra/odoo) | デプロイ/Service/Ingress 等 | ○ 強く推奨 | パスワードを変えたくないなら前回のYAMLを丸ごと継続。Secret/ConfigMapのキー名もそのまま維持。 |
k8s/namespaces.yaml | Namespace作成 | ○ | 既存と同名ならそのまま。 |
scripts/m1_apply.sh / m1_delete.sh | 一括 apply / delete | ○ | 前回のままでOK。パスが変わっていなければ編集不要。 |
補足(パスワードを変えないコツ)
- コードにパスワードは書かない:前回と同じ K8s Secret / .env をそのまま使い回し。
config.pyは既存の環境変数名のまま読めるように、前回版を流用するのが一番ラク。- 例:
DATABASE_URL/ODOO_DATABASE_URLなど、名前を変えない。
- 例:
- Dockerfile/requirements は前回のままが安全(ライブラリ差異で接続ドライバが変わらないように)。
どれを入れ替え・再利用すべきか(実運用のおすすめ)
- そのまま再利用:
api/Dockerfile、api/requirements.txt、k8s/**、scripts/**、api/app/db.py、api/app/config.py、api/chromadb.yaml - 新規・追加:
api/app/services/odoo_intake.py、api/app/services/odoo_upsert.py、api/app/routers/import.py(単数名) - 前回から残して共存可(必要なら):
routers/ai_enrich.pyやservices/chroma_client.pyなど検索/AI系。今回の「取り込みだけ」フェーズでは未使用なので無改修でOK。
最低限の差分だけで動かすチェックリスト
- 前回のDockerfile/requirementsをそのまま使う
- 前回のK8s Secret/ConfigMap(DB/ODOOの接続情報)をそのまま使う
config.pyは前回版を流用(環境変数名変更なし)- 新DDLで
portal_*を作成(前回DBをそのまま使用) routers/import.py+services/odoo_*を追加してmain.pyにマウント/healthが通ることを確認 →POST /import/...で取り込み試験
必要なら、前回のmain.pyに最小差分で新ルーターだけ追加するパッチ例も出せます。パスワード類は一切触らずに進められますよ。
コメントを残す