Add GitOps stack for abrban.com with Gitea Actions CI/CD.
Build and Deploy Platform / build-push-deploy (push) Has been cancelled
Build and Deploy Platform / build-push-deploy (push) Has been cancelled
Harbor in-cluster builds via Kaniko, ArgoCD auto-sync, and production Helm values for abrban.com domains. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
name: Build and Deploy Platform
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# Push via internal Harbor registry (no creds needed from runner pod)
|
||||
REGISTRY_INTERNAL: harbor-registry.cloudhost.svc.cluster.local:5000
|
||||
REGISTRY: registry.abrban.com
|
||||
BACKEND_IMAGE: abrban/cloudhost-backend
|
||||
FRONTEND_IMAGE: abrban/cloudhost-frontend
|
||||
|
||||
jobs:
|
||||
build-push-deploy:
|
||||
runs-on: abrban-kaniko
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set image tag
|
||||
shell: bash
|
||||
run: |
|
||||
SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}"
|
||||
echo "IMAGE_TAG=$(date +%Y%m%d-%H%M)-${SHA:0:8}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build backend (Kaniko)
|
||||
run: |
|
||||
/kaniko/executor \
|
||||
--context=./backend \
|
||||
--dockerfile=./backend/Dockerfile \
|
||||
--destination="${REGISTRY_INTERNAL}/${BACKEND_IMAGE}:${IMAGE_TAG}" \
|
||||
--insecure \
|
||||
--skip-tls-verify
|
||||
|
||||
- name: Build frontend (Kaniko)
|
||||
run: |
|
||||
/kaniko/executor \
|
||||
--context=./frontend \
|
||||
--dockerfile=./frontend/Dockerfile \
|
||||
--build-arg=NEXT_PUBLIC_API_URL=https://api.abrban.com \
|
||||
--destination="${REGISTRY_INTERNAL}/${FRONTEND_IMAGE}:${IMAGE_TAG}" \
|
||||
--insecure \
|
||||
--skip-tls-verify
|
||||
|
||||
- name: Update GitOps values
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i "s|tag: \"[^\"]*\"|tag: \"${IMAGE_TAG}\"|g" gitops/platform/values-abrban.yaml
|
||||
git config user.email "ci@abrban.com"
|
||||
git config user.name "Gitea Actions"
|
||||
git add gitops/platform/values-abrban.yaml
|
||||
git diff --cached --quiet || git commit -m "ci: deploy platform ${IMAGE_TAG}"
|
||||
|
||||
- name: Push GitOps update
|
||||
shell: bash
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git remote set-url origin "https://oauth2:${GITEA_TOKEN}@git.abrban.com/abrban/cloud-host.git"
|
||||
git push origin HEAD:main
|
||||
+18
-1
@@ -131,6 +131,22 @@ kubectl -n cloudhost-builds get cm app-sources -o yaml # BUCKET_NAME
|
||||
|
||||
### ۷. یکپارچهسازی backend
|
||||
|
||||
```bash
|
||||
# کپی secret به namespace پلتفرم (یکبار)
|
||||
kubectl -n cloudhost-builds get secret ceph-app-sources-credentials -o yaml \
|
||||
| sed 's/namespace: cloudhost-builds/namespace: cloudhost/' \
|
||||
| kubectl apply -f -
|
||||
|
||||
# یا با Helm (پیشنهادی):
|
||||
helm upgrade cloudhost ./backend/helm/cloudhost-platform -n cloudhost \
|
||||
--set backend.sourceStorage.enabled=true \
|
||||
--set backend.env.PLATFORM_STORAGE_CLASS=rook-ceph-block \
|
||||
--set backend.env.PLATFORM_CREATE_STORAGE_CLASS=false \
|
||||
--set backend.env.PLATFORM_STORAGE_PROVISIONER=rook-ceph.rbd.csi.ceph.com
|
||||
```
|
||||
|
||||
بدون Helm میتوانید دستی patch کنید:
|
||||
|
||||
```bash
|
||||
kubectl -n cloudhost set env deploy/cloudhost-backend \
|
||||
PLATFORM_STORAGE_CLASS=rook-ceph-block \
|
||||
@@ -324,6 +340,7 @@ helm upgrade cloudhost-ceph . -n cloudhost-builds -f values.yaml
|
||||
- [ ] `rook-ceph-block` و `rook-ceph-bucket` در `kubectl get sc`
|
||||
- [ ] `ceph-app-sources-credentials` در `cloudhost-builds` و `cloudhost`
|
||||
- [ ] env بکاند: `PLATFORM_STORAGE_CLASS=rook-ceph-block`
|
||||
- [ ] `SOURCE_STORAGE_*` در backend از secret خوانده میشود
|
||||
- [x] `SOURCE_STORAGE_*` در backend از secret خوانده میشود (`backend.sourceStorage.enabled=true` در Helm)
|
||||
- [ ] اپ تست: آپلود zip و deploy با bucket فعال
|
||||
- [ ] اپ تست با PVC جدید deploy شده
|
||||
- [ ] ایمیجهای Rook در Harbor موجود و pull تست شده
|
||||
|
||||
@@ -19,6 +19,10 @@ spec:
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.backend.fullname" . }}
|
||||
spec:
|
||||
{{- with .Values.backend.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
initContainers:
|
||||
{{- if .Values.postgres.enabled }}
|
||||
- name: wait-postgres
|
||||
@@ -88,6 +92,11 @@ spec:
|
||||
- name: {{ $key }}
|
||||
value: {{ $val | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.backend.sourceStorage.enabled }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: {{ .Values.backend.sourceStorage.existingSecret }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: uploads
|
||||
mountPath: /app/uploads
|
||||
|
||||
@@ -17,6 +17,10 @@ spec:
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.frontend.fullname" . }}
|
||||
spec:
|
||||
{{- with .Values.frontend.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: frontend
|
||||
image: {{ include "cloudhost-platform.frontendImage" . | quote }}
|
||||
|
||||
@@ -7,7 +7,7 @@ metadata:
|
||||
labels:
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
{{- if and .Values.ingress.tls.enabled (not .Values.ingress.tls.secretName) }}
|
||||
cert-manager.io/cluster-issuer: {{ .Values.ingress.tls.clusterIssuer | quote }}
|
||||
{{- end }}
|
||||
{{- if and .Values.ingress.singleHost.enabled .Values.ingress.singleHost.apiPath }}
|
||||
|
||||
@@ -36,6 +36,10 @@ ingress:
|
||||
clusterIssuer: letsencrypt-prod
|
||||
|
||||
backend:
|
||||
# Enable after copying ceph-app-sources-credentials secret into the cloudhost namespace
|
||||
sourceStorage:
|
||||
enabled: false
|
||||
existingSecret: ceph-app-sources-credentials
|
||||
env:
|
||||
PLATFORM_DOMAIN: apps.example.com
|
||||
REGISTRY_URL: registry.cloudhost-builds.svc.cluster.local:5000
|
||||
|
||||
@@ -42,8 +42,13 @@ redis:
|
||||
backend:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
imagePullSecrets:
|
||||
- name: registry-pull-secret
|
||||
uploads:
|
||||
size: 20Gi
|
||||
sourceStorage:
|
||||
enabled: false
|
||||
existingSecret: ceph-app-sources-credentials
|
||||
resources: {}
|
||||
extraEnv: {}
|
||||
env:
|
||||
@@ -66,6 +71,8 @@ backend:
|
||||
frontend:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
imagePullSecrets:
|
||||
- name: registry-pull-secret
|
||||
resources: {}
|
||||
|
||||
# JWT secrets — set in production (values-production.example.yaml)
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
# Maddy mail server — lightweight full mail server (SMTP + IMAP + DKIM)
|
||||
# Namespace: mail | Host: mail.abrban.com | Primary domain: abrban.com
|
||||
#
|
||||
# Exposed on node IP 78.157.39.52 via k3s servicelb (klipper).
|
||||
# TLS uses the *.abrban.com wildcard cert (secret abrban-wildcard-tls, copied into ns mail).
|
||||
#
|
||||
# NOTE (Iran/IP reputation): inbound mail (receiving) works; outbound delivery to
|
||||
# Gmail/Outlook may be blocked or land in spam, and outbound port 25 may be filtered
|
||||
# by the ISP. Use a smarthost relay if real external delivery is required.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: maddy-config
|
||||
namespace: mail
|
||||
data:
|
||||
maddy.conf: |
|
||||
## Maddy Mail Server - configuration (mail.abrban.com)
|
||||
|
||||
$(hostname) = mail.abrban.com
|
||||
$(primary_domain) = abrban.com
|
||||
$(local_domains) = $(primary_domain)
|
||||
|
||||
tls file /etc/maddy/tls/tls.crt /etc/maddy/tls/tls.key
|
||||
|
||||
# ---- Local storage & authentication ----
|
||||
storage.imapsql local_mailboxes {
|
||||
driver sqlite3
|
||||
dsn imapsql.db
|
||||
}
|
||||
|
||||
auth.pass_table local_authdb {
|
||||
table sql_table {
|
||||
driver sqlite3
|
||||
dsn credentials.db
|
||||
table_name passwords
|
||||
}
|
||||
}
|
||||
|
||||
# ---- Routing ----
|
||||
hostname $(hostname)
|
||||
|
||||
table.chain local_rewrites {
|
||||
optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
|
||||
optional_step static {
|
||||
entry postmaster postmaster@$(primary_domain)
|
||||
}
|
||||
optional_step file /data/aliases
|
||||
}
|
||||
|
||||
msgpipeline local_routing {
|
||||
destination postmaster $(local_domains) {
|
||||
modify {
|
||||
replace_rcpt &local_rewrites
|
||||
}
|
||||
deliver_to &local_mailboxes
|
||||
}
|
||||
default_destination {
|
||||
reject 550 5.1.1 "User doesn't exist"
|
||||
}
|
||||
}
|
||||
|
||||
# ---- Inbound SMTP (port 25) ----
|
||||
smtp tcp://0.0.0.0:25 {
|
||||
limits {
|
||||
all rate 20 1s
|
||||
all concurrency 10
|
||||
}
|
||||
dmarc yes
|
||||
check {
|
||||
require_mx_record
|
||||
dkim
|
||||
spf
|
||||
}
|
||||
source $(local_domains) {
|
||||
reject 501 5.1.8 "Use Submission for outgoing SMTP"
|
||||
}
|
||||
default_source {
|
||||
destination postmaster $(local_domains) {
|
||||
deliver_to &local_routing
|
||||
}
|
||||
default_destination {
|
||||
reject 550 5.1.1 "User doesn't exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---- Submission (ports 465 implicit-TLS, 587 STARTTLS) ----
|
||||
submission tls://0.0.0.0:465 tcp://0.0.0.0:587 {
|
||||
limits {
|
||||
all rate 50 1s
|
||||
}
|
||||
auth &local_authdb
|
||||
source $(local_domains) {
|
||||
check {
|
||||
authorize_sender {
|
||||
prepare_email &local_rewrites
|
||||
user_to_email identity
|
||||
}
|
||||
}
|
||||
destination postmaster $(local_domains) {
|
||||
deliver_to &local_routing
|
||||
}
|
||||
default_destination {
|
||||
modify {
|
||||
dkim $(primary_domain) $(hostname) default
|
||||
}
|
||||
deliver_to &remote_queue
|
||||
}
|
||||
}
|
||||
default_source {
|
||||
reject 501 5.1.8 "Non-local sender domain"
|
||||
}
|
||||
}
|
||||
|
||||
# ---- Outbound delivery queue ----
|
||||
target.remote outbound_delivery {
|
||||
limits {
|
||||
destination rate 20 1s
|
||||
destination concurrency 10
|
||||
}
|
||||
mx_auth {
|
||||
dane
|
||||
mtasts {
|
||||
cache fs
|
||||
fs_dir mtasts_cache/
|
||||
}
|
||||
local_policy {
|
||||
min_tls_level encrypted
|
||||
min_mx_level none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target.queue remote_queue {
|
||||
target &outbound_delivery
|
||||
autogenerated_msg_domain $(primary_domain)
|
||||
bounce {
|
||||
destination postmaster $(local_domains) {
|
||||
deliver_to &local_routing
|
||||
}
|
||||
default_destination {
|
||||
reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---- IMAP (993 implicit-TLS, 143 STARTTLS) ----
|
||||
imap tls://0.0.0.0:993 tcp://0.0.0.0:143 {
|
||||
auth &local_authdb
|
||||
storage &local_mailboxes
|
||||
}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: maddy-data
|
||||
namespace: mail
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: maddy
|
||||
namespace: mail
|
||||
labels:
|
||||
app: maddy
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: maddy
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: maddy
|
||||
spec:
|
||||
containers:
|
||||
- name: maddy
|
||||
image: foxcpp/maddy:0.7
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: MADDY_HOSTNAME
|
||||
value: mail.abrban.com
|
||||
- name: MADDY_DOMAIN
|
||||
value: abrban.com
|
||||
ports:
|
||||
- { name: smtp, containerPort: 25 }
|
||||
- { name: submission, containerPort: 587 }
|
||||
- { name: smtps, containerPort: 465 }
|
||||
- { name: imap, containerPort: 143 }
|
||||
- { name: imaps, containerPort: 993 }
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: config
|
||||
mountPath: /data/maddy.conf
|
||||
subPath: maddy.conf
|
||||
- name: tls
|
||||
mountPath: /etc/maddy/tls
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 256Mi
|
||||
livenessProbe:
|
||||
tcpSocket: { port: 25 }
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: maddy-data
|
||||
- name: config
|
||||
configMap:
|
||||
name: maddy-config
|
||||
- name: tls
|
||||
secret:
|
||||
secretName: abrban-wildcard-tls
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: maddy
|
||||
namespace: mail
|
||||
labels:
|
||||
app: maddy
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
externalTrafficPolicy: Local # preserve client source IP (needed for SPF/spam checks)
|
||||
selector:
|
||||
app: maddy
|
||||
ports:
|
||||
- { name: smtp, port: 25, targetPort: 25 }
|
||||
- { name: submission, port: 587, targetPort: 587 }
|
||||
- { name: smtps, port: 465, targetPort: 465 }
|
||||
- { name: imap, port: 143, targetPort: 143 }
|
||||
- { name: imaps, port: 993, targetPort: 993 }
|
||||
Generated
+425
-28
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "cloudhost-backend",
|
||||
"name": "abrban-backend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cloudhost-backend",
|
||||
"name": "abrban-backend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1077.0",
|
||||
"@kubernetes/client-node": "^1.4.0",
|
||||
"@nestjs/bull": "^11.0.4",
|
||||
"@nestjs/common": "^11.1.24",
|
||||
@@ -180,6 +181,314 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/checksums": {
|
||||
"version": "3.1000.10",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.10.tgz",
|
||||
"integrity": "sha512-OUNjNcyA8Ai2OdlRUxW5jHUf6XJmqqZk3UddL+mDiUCtXrVqdmIHHkdDFWBlBRjhore/3ZBMgRXgcS0ggtvD0Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-s3": {
|
||||
"version": "3.1077.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1077.0.tgz",
|
||||
"integrity": "sha512-yWK6jOMrMUgGarXlrQaAopWXva20NaOqTPApuE68SzsBFQ57fQ5E13BKUPLwtPgymk+8L6vG/O4h7Q30IBYr2w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/checksums": "^3.1000.10",
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.60",
|
||||
"@aws-sdk/middleware-sdk-s3": "^3.972.56",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.37",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/fetch-http-handler": "^5.6.1",
|
||||
"@smithy/node-http-handler": "^4.9.1",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/core": {
|
||||
"version": "3.974.25",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.25.tgz",
|
||||
"integrity": "sha512-fJFkx6u6wCqGMV/v6EAxiwa2UzEukbvr1hNPv4MrD3yj4IFz011jZg42/eSTOP/u5kJ0tlILqEjCWtT8GiKZvA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@aws-sdk/xml-builder": "^3.972.32",
|
||||
"@aws/lambda-invoke-store": "^0.2.2",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/signature-v4": "^5.6.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"bowser": "^2.11.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-env": {
|
||||
"version": "3.972.51",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.51.tgz",
|
||||
"integrity": "sha512-Xo+/zf5k5pZdo53X8aVXN4MJGfU/M1P7yMM/GbNY/x9fyRZGEzjhKqW38GA0FSQQ9TYKs+bfPyz5ja4bi6pjTQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-http": {
|
||||
"version": "3.972.53",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.53.tgz",
|
||||
"integrity": "sha512-7E9oFUcf9YWe+ttGiWhe/cCSI+pswwelzgQMoKXgPJi1AIfS27TK6et5ZULqEqHu30zbN+jh1RqlwcXqY/aXyg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/fetch-http-handler": "^5.6.1",
|
||||
"@smithy/node-http-handler": "^4.9.1",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-ini": {
|
||||
"version": "3.972.58",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.58.tgz",
|
||||
"integrity": "sha512-MPr0hD8pyDGfF3dWXvFOILhcKTB9ptqJOJK9JEuDQzpc2HgKisY16eR7IrKUXxSbz8LZj+LHz/CS8Y5G1ai7yw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.51",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.53",
|
||||
"@aws-sdk/credential-provider-login": "^3.972.57",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.51",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.57",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.57",
|
||||
"@aws-sdk/nested-clients": "^3.997.25",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/credential-provider-imds": "^4.4.4",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-login": {
|
||||
"version": "3.972.57",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.57.tgz",
|
||||
"integrity": "sha512-kPWc/SCrl9agKeywxKwPEoQHanWag0LcNQrcZpEQpjNifkxq6tQENhgrrS9al317CF6yytyihlX+FhPHlk0QjA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/nested-clients": "^3.997.25",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-node": {
|
||||
"version": "3.972.60",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.60.tgz",
|
||||
"integrity": "sha512-hE2hIBJQjCDRx8TbSqpVQ+/o2mIrJZQZbQ3LlwE2bJf7z47x5GmhcvGwZPqJH7Oq//SzTXEBGSZ4qSpK3yPbhw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-provider-env": "^3.972.51",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.53",
|
||||
"@aws-sdk/credential-provider-ini": "^3.972.58",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.51",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.57",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.57",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/credential-provider-imds": "^4.4.4",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-process": {
|
||||
"version": "3.972.51",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.51.tgz",
|
||||
"integrity": "sha512-081dD2RlnmY+G05v6E73KfACvDjPjnttrLjGHE2SSglbID25UcuijbWpL4g+XR5T2Kl4oIJoVBXi64s+2f009Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-sso": {
|
||||
"version": "3.972.57",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.57.tgz",
|
||||
"integrity": "sha512-dC7ZyX3EHKHLOeVUEDzzGvk0L1s6N06YDrau7P0rGXL/j1cO+DzN2w1x9vcEh7zljVCR3019f5mi1Th+GGTURw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/nested-clients": "^3.997.25",
|
||||
"@aws-sdk/token-providers": "3.1077.0",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
||||
"version": "3.972.57",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.57.tgz",
|
||||
"integrity": "sha512-HtWM3FV2o7NJFJSUqFLBlxmV9RxQRHpzCvQaP1n1Qo4CxQSvwpJ8ERWHiLqXMFDgDXyELt+EZNFcpG6XQRcJbQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/nested-clients": "^3.997.25",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-sdk-s3": {
|
||||
"version": "3.972.56",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.56.tgz",
|
||||
"integrity": "sha512-VFLd7bT8ef48H2n2iDrY/w9EPpXLmC0v4NEriXy2vuTgEP/FrKqrcwi7eobhcOrdO37uLbMt5AWZlY55R2/xqg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.37",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/nested-clients": {
|
||||
"version": "3.997.25",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.25.tgz",
|
||||
"integrity": "sha512-VpRQ3wR6l+fwRHV5veJL2ehtyQFrGyH/2CJG9DVtb8H3xyqqnZWSTSrq/CJJ7DvDlDgrPRiW2SkYA8pN6VWCFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.37",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/fetch-http-handler": "^5.6.1",
|
||||
"@smithy/node-http-handler": "^4.9.1",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/signature-v4-multi-region": {
|
||||
"version": "3.996.37",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.37.tgz",
|
||||
"integrity": "sha512-u8qd064XsHzM0Mk+yH4IPKn/ZC9rdniEKs+neBHNlsPZirw3rcLvmrH4ImoKC4yF7A0I/MbcC3dseARnJLiAhg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/signature-v4": "^5.6.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/token-providers": {
|
||||
"version": "3.1077.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1077.0.tgz",
|
||||
"integrity": "sha512-sRUkfZ3fpOco95jZHsQUQiXvuIVLvCmWVclFg6dRFDyfsYs6Pdr/NuZ2+yJxeHN+6WAfDh2aZ8nlZntnvuhZUQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.25",
|
||||
"@aws-sdk/nested-clients": "^3.997.25",
|
||||
"@aws-sdk/types": "^3.973.14",
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/types": {
|
||||
"version": "3.973.14",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.14.tgz",
|
||||
"integrity": "sha512-vH4pEu9YBEwr67yT+GVcmKX0GzfIrIYUn+MF5vXg9OspouVnAekuyVyawFvZHEK7WlcwVDwNrqI3ZBDUAiyu9A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/xml-builder": {
|
||||
"version": "3.972.32",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.32.tgz",
|
||||
"integrity": "sha512-2loKuOMRFDg1nwdni5AtJ9S5juVbRNPNsPC7tWTfkHyycPwACMhxepspUHi8GhvfNlL2cQo3sPMod1uib+KZ0w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws/lambda-invoke-store": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz",
|
||||
"integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
@@ -211,7 +520,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -742,6 +1050,31 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
@@ -749,6 +1082,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
@@ -2278,7 +2612,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.26.tgz",
|
||||
"integrity": "sha512-0VARQyzuGbprvjO+slWq9Jtj1P0jYCSKAUSv9LWFNWD39ZbDzXXM1pMs35kReVXwchra0urMfTQxw4uAOfdSzA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"file-type": "21.3.4",
|
||||
"iterare": "1.2.1",
|
||||
@@ -2325,7 +2658,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.26.tgz",
|
||||
"integrity": "sha512-K45zUwYpowEsVqm8qNIzsMcl4LJev0MK9zVhDnmym7YRTJ2/caslqVeKYhPRd5+Fh81IkvWUVu6vEo46uZ5mgQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-safe-stringify": "2.1.1",
|
||||
"iterare": "1.2.1",
|
||||
@@ -2408,7 +2740,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.26.tgz",
|
||||
"integrity": "sha512-MJ5Kwe52Ag4nlIuLK2ekB6TVYu1a22uvDzc0Aq0wIzcLySIz4YK0fMcrDOKGdbGQWpfZtNu1PM3jhlf4hvf6Og==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cors": "2.8.6",
|
||||
"express": "5.2.1",
|
||||
@@ -2660,6 +2991,87 @@
|
||||
"@sinonjs/commons": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/core": {
|
||||
"version": "3.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.28.0.tgz",
|
||||
"integrity": "sha512-N/LoLG8pZ1zv5cIWpdF6vmSjtZtXKK9G0OqT5yYCOZU+CzPq1+nYA95VoKJBGWRScs7YbMugZ7lZx8Fj1vdHoA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/credential-provider-imds": {
|
||||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.4.tgz",
|
||||
"integrity": "sha512-jT0WrDaM88L5na9FX1xRNywCS3B1n75wPY5Ksasjo0PHUtuI7d8FclksN1BbOSYTiaiKxUDqU23nUymH/V+AaQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/fetch-http-handler": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.1.tgz",
|
||||
"integrity": "sha512-fW6l9rWoyk1iyzfuZaERnZLNjB6WIojgGm6Bo9Hpfpy3RUpltjLikNlxTsS/YtxVobcfbCGBuAncREYqT4hvqQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/node-http-handler": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.1.tgz",
|
||||
"integrity": "sha512-m/f15di58P6NtLQ7eVEb5N19NdJWn+4c7zfkFHMT/i3JH7U8UtknpPoy8o2tm2R3OdliYvsvQhZHIfACQDqT+Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/signature-v4": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.0.tgz",
|
||||
"integrity": "sha512-IkPHQdbyoebSwBCuMTzJ/2oIhKVqiZZAZxQYSlpDZqq/WhJUpmdgbHvP7ItddxsPzcDUJeI0V4PNMSNtlZ0aqA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.28.0",
|
||||
"@smithy/types": "^4.15.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "4.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.0.tgz",
|
||||
"integrity": "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sqltools/formatter": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
|
||||
@@ -2955,7 +3367,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
|
||||
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
@@ -3143,7 +3554,6 @@
|
||||
"integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.61.0",
|
||||
"@typescript-eslint/types": "8.61.0",
|
||||
@@ -3915,7 +4325,6 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3974,7 +4383,6 @@
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -4498,6 +4906,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/bowser": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
|
||||
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
||||
@@ -4529,7 +4943,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@@ -4609,7 +5022,6 @@
|
||||
"resolved": "https://registry.npmjs.org/bull/-/bull-4.16.5.tgz",
|
||||
"integrity": "sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.9.0",
|
||||
"get-port": "^5.1.1",
|
||||
@@ -4762,7 +5174,6 @@
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
@@ -4810,15 +5221,13 @@
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-validator": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz",
|
||||
"integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/validator": "^13.15.3",
|
||||
"libphonenumber-js": "^1.11.1",
|
||||
@@ -5504,7 +5913,6 @@
|
||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -6935,7 +7343,6 @@
|
||||
"integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.4.2",
|
||||
"@jest/types": "30.4.1",
|
||||
@@ -7649,7 +8056,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz",
|
||||
"integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
}
|
||||
@@ -8595,7 +9001,6 @@
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1",
|
||||
@@ -8717,7 +9122,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz",
|
||||
"integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.13.0",
|
||||
"pg-pool": "^3.14.0",
|
||||
@@ -8965,7 +9369,6 @@
|
||||
"integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -9321,7 +9724,6 @@
|
||||
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -10327,7 +10729,6 @@
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -10473,7 +10874,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-1.0.0.tgz",
|
||||
"integrity": "sha512-2mSKNqucP8vo+xQLP59xlHUcqLvG6qajxA7q7tnhJgeZjTrA6lK/Ar7LRyiAxdXhyXmGbIPsArPmcUB9Xg+M7w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sqltools/formatter": "^1.2.5",
|
||||
"ansis": "^4.2.0",
|
||||
@@ -10687,7 +11087,6 @@
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -10950,7 +11349,6 @@
|
||||
"integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
@@ -11186,7 +11584,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "cloudhost-backend",
|
||||
"name": "abrban-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "CloudHost PaaS Backend API",
|
||||
"private": true,
|
||||
@@ -24,6 +24,7 @@
|
||||
"sync:migrations": "node scripts/sync-helm-migrations.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1077.0",
|
||||
"@kubernetes/client-node": "^1.4.0",
|
||||
"@nestjs/bull": "^11.0.4",
|
||||
"@nestjs/common": "^11.1.24",
|
||||
|
||||
@@ -18,6 +18,7 @@ import { LifecycleModule } from './lifecycle/lifecycle.module';
|
||||
import { ApplicationMigrationsModule } from './application-migrations/application-migrations.module';
|
||||
import { AdminModule } from './admin/admin.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { StorageModule } from './storage/storage.module';
|
||||
import configuration from './config/configuration';
|
||||
|
||||
@Module({
|
||||
@@ -66,6 +67,7 @@ import configuration from './config/configuration';
|
||||
]),
|
||||
|
||||
// Feature modules
|
||||
StorageModule,
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
ApplicationsModule,
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
assertRuntimeMatch,
|
||||
detectRuntimeFromArchive,
|
||||
} from '../build/runtime-detector';
|
||||
import { SourceStorageService } from '../storage/source-storage.service';
|
||||
import * as os from 'os';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationsService {
|
||||
@@ -31,6 +33,7 @@ export class ApplicationsService {
|
||||
private appsRepository: Repository<Application>,
|
||||
private clustersService: ClustersService,
|
||||
private configService: ConfigService,
|
||||
private sourceStorage: SourceStorageService,
|
||||
) {}
|
||||
|
||||
private toDnsLabel(value: string): string {
|
||||
@@ -208,17 +211,12 @@ export class ApplicationsService {
|
||||
async delete(id: string, userId: string): Promise<Application> {
|
||||
const app = await this.findOne(id, userId);
|
||||
|
||||
// Delete uploaded files
|
||||
// Delete uploaded source files
|
||||
if (app.codePath) {
|
||||
try {
|
||||
const uploadDir = this.configService.get<string>('platform.uploadDir') || './uploads';
|
||||
const appDir = path.join(uploadDir, app.userId, app.id);
|
||||
if (fs.existsSync(appDir)) {
|
||||
fs.rmSync(appDir, { recursive: true, force: true });
|
||||
this.logger.log(`Deleted upload directory: ${appDir}`);
|
||||
}
|
||||
await this.sourceStorage.deleteSource(app.userId, app.id, app.codePath);
|
||||
} catch (e: any) {
|
||||
this.logger.warn(`Failed to delete upload dir for ${app.name}: ${e.message}`);
|
||||
this.logger.warn(`Failed to delete source for ${app.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,21 +263,15 @@ export class ApplicationsService {
|
||||
}
|
||||
|
||||
const app = await this.findOne(id, userId);
|
||||
const uploadDir = this.configService.get<string>('platform.uploadDir') || './uploads';
|
||||
const appDir = path.join(uploadDir, app.userId, app.id);
|
||||
|
||||
// Ensure directory exists
|
||||
fs.mkdirSync(appDir, { recursive: true });
|
||||
|
||||
// Save the zip file
|
||||
const zipPath = path.join(appDir, 'source.zip');
|
||||
fs.writeFileSync(zipPath, file.buffer);
|
||||
const tempPath = path.join(os.tmpdir(), `upload-${app.id}-${Date.now()}.zip`);
|
||||
fs.writeFileSync(tempPath, file.buffer);
|
||||
|
||||
try {
|
||||
const detected = await detectRuntimeFromArchive(zipPath);
|
||||
const detected = await detectRuntimeFromArchive(tempPath);
|
||||
assertRuntimeMatch(app.runtime, detected);
|
||||
|
||||
app.codePath = zipPath;
|
||||
const storedPath = await this.sourceStorage.putSource(app.userId, app.id, file.buffer);
|
||||
app.codePath = storedPath;
|
||||
const saved = await this.appsRepository.save(app);
|
||||
|
||||
if (detected.confidence === 'low') {
|
||||
@@ -289,13 +281,19 @@ export class ApplicationsService {
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(`Uploaded code for ${app.name} → ${zipPath} (${(file.size / 1024).toFixed(1)} KB)`);
|
||||
this.logger.log(`Uploaded code for ${app.name} → ${storedPath} (${(file.size / 1024).toFixed(1)} KB)`);
|
||||
return saved;
|
||||
} catch (err) {
|
||||
if (fs.existsSync(zipPath)) {
|
||||
fs.unlinkSync(zipPath);
|
||||
try {
|
||||
await this.sourceStorage.deleteSource(app.userId, app.id);
|
||||
} catch {
|
||||
// ignore rollback errors
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
if (fs.existsSync(tempPath)) {
|
||||
fs.unlinkSync(tempPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AppRuntime } from '../common/enums';
|
||||
import { ClustersService } from '../clusters/clusters.service';
|
||||
import { RegistryService } from '../kubernetes/registry.service';
|
||||
import { BuildProgressStore } from './build-progress.store';
|
||||
import { SourceStorageService } from '../storage/source-storage.service';
|
||||
|
||||
describe('BuildService', () => {
|
||||
let service: BuildService;
|
||||
@@ -34,6 +35,14 @@ describe('BuildService', () => {
|
||||
useValue: { get: jest.fn(), set: jest.fn(), clear: jest.fn() },
|
||||
},
|
||||
{ provide: RegistryService, useValue: {} },
|
||||
{
|
||||
provide: SourceStorageService,
|
||||
useValue: {
|
||||
isObjectStorage: () => false,
|
||||
materializeToTempFile: jest.fn(),
|
||||
getSize: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { AppRuntime } from '../common/enums';
|
||||
import { ClustersService } from '../clusters/clusters.service';
|
||||
import { RegistryService } from '../kubernetes/registry.service';
|
||||
import { BuildProgressStore } from './build-progress.store';
|
||||
import { SourceStorageService } from '../storage/source-storage.service';
|
||||
import {
|
||||
detectDjangoSettingsModule,
|
||||
detectGoBuildTarget,
|
||||
@@ -64,6 +65,7 @@ export class BuildService {
|
||||
private clustersService: ClustersService,
|
||||
private registryService: RegistryService,
|
||||
private progressStore: BuildProgressStore,
|
||||
private sourceStorage: SourceStorageService,
|
||||
) {}
|
||||
|
||||
private beginBuildSession(deploymentId: string): void {
|
||||
@@ -321,13 +323,23 @@ export class BuildService {
|
||||
this.beginBuildSession(deploymentId);
|
||||
}
|
||||
|
||||
const codePath = app.codePath ? path.resolve(app.codePath) : null;
|
||||
const hasUploadedCode = codePath && fs.existsSync(codePath);
|
||||
const hasUploadedCode = !!app.codePath;
|
||||
let localZipPath: string | null = null;
|
||||
let cleanupSource: (() => void) | null = null;
|
||||
|
||||
if (hasUploadedCode) {
|
||||
await validateRuntimeFromArchive(app.runtime, codePath);
|
||||
try {
|
||||
const materialized = await this.sourceStorage.materializeToTempFile(app.codePath!);
|
||||
localZipPath = materialized.path;
|
||||
cleanupSource = materialized.cleanup;
|
||||
await validateRuntimeFromArchive(app.runtime, localZipPath);
|
||||
} catch (e) {
|
||||
cleanupSource?.();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const archiveEntries = hasUploadedCode ? await listArchiveEntries(codePath!) : [];
|
||||
const archiveEntries = hasUploadedCode && localZipPath ? await listArchiveEntries(localZipPath) : [];
|
||||
|
||||
// Determine Dockerfile based on runtime
|
||||
const dockerfileContent = this.generateDockerfile(app, archiveEntries);
|
||||
@@ -377,16 +389,18 @@ export class BuildService {
|
||||
|
||||
// If we have uploaded code, create a PVC and upload via kubectl cp
|
||||
let sourcePvcName: string | undefined;
|
||||
if (hasUploadedCode) {
|
||||
if (hasUploadedCode && localZipPath) {
|
||||
sourcePvcName = `${buildPodName}-source`;
|
||||
if (deploymentId) {
|
||||
this.updateBuildSession(deploymentId, { sourcePvcName });
|
||||
}
|
||||
const zipSize = fs.statSync(codePath!).size;
|
||||
const zipSize = await this.sourceStorage.getSize(app.codePath!);
|
||||
// Allocate PVC size = zip size * 3 (zip + extracted), min 1Gi
|
||||
const pvcSizeGi = Math.max(1, Math.ceil((zipSize * 3) / (1024 * 1024 * 1024)));
|
||||
|
||||
await this.uploadSourceViaPVC(kc, coreApi, buildNamespace!, sourcePvcName, codePath!, pvcSizeGi, deploymentId);
|
||||
await this.uploadSourceViaPVC(kc, coreApi, buildNamespace!, sourcePvcName, localZipPath, pvcSizeGi, deploymentId);
|
||||
cleanupSource?.();
|
||||
cleanupSource = null;
|
||||
}
|
||||
|
||||
// Build the Kaniko Job spec
|
||||
@@ -653,6 +667,7 @@ export class BuildService {
|
||||
this.logger.warn(`Failed to clean up ConfigMap: ${e.message}`);
|
||||
}
|
||||
this.endBuildSession(deploymentId);
|
||||
cleanupSource?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,14 @@ export default () => ({
|
||||
},
|
||||
},
|
||||
|
||||
sourceStorage: {
|
||||
endpoint: process.env.SOURCE_STORAGE_ENDPOINT,
|
||||
region: process.env.SOURCE_STORAGE_REGION || 'us-east-1',
|
||||
bucket: process.env.SOURCE_STORAGE_BUCKET,
|
||||
accessKey: process.env.SOURCE_STORAGE_ACCESS_KEY,
|
||||
secretKey: process.env.SOURCE_STORAGE_SECRET_KEY,
|
||||
},
|
||||
|
||||
platform: {
|
||||
domain: resolvePlatformDomainFromEnv(),
|
||||
previewRootDomain: resolvePreviewRootDomainFromEnv(),
|
||||
|
||||
@@ -15,6 +15,7 @@ import * as path from 'path';
|
||||
import { AppSnapshot, SnapshotType, SnapshotStatus } from './entities/snapshot.entity';
|
||||
import { ApplicationsService } from '../applications/applications.service';
|
||||
import { KubernetesService } from '../kubernetes/kubernetes.service';
|
||||
import { SourceStorageService } from '../storage/source-storage.service';
|
||||
import { AppRuntime, DatabaseType, ProductType } from '../common/enums';
|
||||
|
||||
const MAX_SNAPSHOTS = 10;
|
||||
@@ -30,6 +31,7 @@ export class SnapshotsService implements OnModuleInit {
|
||||
private applicationsService: ApplicationsService,
|
||||
private kubernetesService: KubernetesService,
|
||||
private configService: ConfigService,
|
||||
private sourceStorage: SourceStorageService,
|
||||
) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
@@ -122,13 +124,18 @@ export class SnapshotsService implements OnModuleInit {
|
||||
|
||||
if (!managedDbOnly) {
|
||||
// 1. Copy current source code zip
|
||||
if (app.codePath && fs.existsSync(app.codePath)) {
|
||||
if (app.codePath && (await this.sourceStorage.exists(app.codePath))) {
|
||||
await this.setSnapshotProgress(snapshotId, 12);
|
||||
const destPath = path.join(snapshotDir, 'source.zip');
|
||||
fs.copyFileSync(app.codePath, destPath);
|
||||
updates.appArchivePath = destPath;
|
||||
updates.appArchiveSize = fs.statSync(destPath).size;
|
||||
this.logger.log(`Snapshot ${snapshotId}: copied source code (${(updates.appArchiveSize / 1024).toFixed(1)} KB)`);
|
||||
const { path: tempPath, cleanup } = await this.sourceStorage.materializeToTempFile(app.codePath);
|
||||
try {
|
||||
const destPath = path.join(snapshotDir, 'source.zip');
|
||||
fs.copyFileSync(tempPath, destPath);
|
||||
updates.appArchivePath = destPath;
|
||||
updates.appArchiveSize = fs.statSync(destPath).size;
|
||||
this.logger.log(`Snapshot ${snapshotId}: copied source code (${(updates.appArchiveSize / 1024).toFixed(1)} KB)`);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Archive wp-content for WordPress apps
|
||||
@@ -315,8 +322,9 @@ export class SnapshotsService implements OnModuleInit {
|
||||
async downloadCurrentSource(applicationId: string, userId: string): Promise<{ filePath: string; fileName: string } | null> {
|
||||
const app = await this.applicationsService.findOne(applicationId, userId);
|
||||
|
||||
if (app.codePath && fs.existsSync(app.codePath)) {
|
||||
return { filePath: app.codePath, fileName: `${app.name}-current-source.zip` };
|
||||
if (app.codePath && (await this.sourceStorage.exists(app.codePath))) {
|
||||
const { path } = await this.sourceStorage.materializeToTempFile(app.codePath);
|
||||
return { filePath: path, fileName: `${app.name}-current-source.zip` };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -382,12 +390,9 @@ export class SnapshotsService implements OnModuleInit {
|
||||
} else {
|
||||
// Revision not found — fall back to restoring source code and redeploying
|
||||
if (snapshot.appArchivePath && fs.existsSync(snapshot.appArchivePath)) {
|
||||
const uploadDir = this.configService.get<string>('platform.uploadDir') || './uploads';
|
||||
const appDir = path.join(uploadDir, app.userId, app.id);
|
||||
const destPath = path.join(appDir, 'source.zip');
|
||||
fs.mkdirSync(appDir, { recursive: true });
|
||||
fs.copyFileSync(snapshot.appArchivePath, destPath);
|
||||
await this.applicationsService.update(app.id, app.userId, { codePath: destPath } as any);
|
||||
const buffer = fs.readFileSync(snapshot.appArchivePath);
|
||||
const storedPath = await this.sourceStorage.putSource(app.userId, app.id, buffer);
|
||||
await this.applicationsService.update(app.id, app.userId, { codePath: storedPath } as any);
|
||||
details.push('✅ Source code restored (K8s revision expired — will need redeploy)');
|
||||
this.logger.log(`Rollback ${snapshotId}: source code restored (revision not found)`);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { SourceStorageService } from './source-storage.service';
|
||||
|
||||
jest.mock('@aws-sdk/client-s3', () => {
|
||||
const send = jest.fn();
|
||||
return {
|
||||
S3Client: jest.fn().mockImplementation(() => ({ send })),
|
||||
PutObjectCommand: jest.fn().mockImplementation((input) => ({ input })),
|
||||
GetObjectCommand: jest.fn(),
|
||||
HeadObjectCommand: jest.fn(),
|
||||
DeleteObjectCommand: jest.fn(),
|
||||
__mockSend: send,
|
||||
};
|
||||
});
|
||||
|
||||
const s3Module = jest.requireMock('@aws-sdk/client-s3');
|
||||
const mockSend = s3Module.__mockSend as jest.Mock;
|
||||
|
||||
describe('SourceStorageService', () => {
|
||||
let service: SourceStorageService;
|
||||
let uploadDir: string;
|
||||
|
||||
const createModule = (config: Record<string, string | undefined>) => {
|
||||
return Test.createTestingModule({
|
||||
providers: [
|
||||
SourceStorageService,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: (key: string) => {
|
||||
const map: Record<string, string | undefined> = {
|
||||
'platform.uploadDir': uploadDir,
|
||||
'sourceStorage.endpoint': config.endpoint,
|
||||
'sourceStorage.region': config.region,
|
||||
'sourceStorage.bucket': config.bucket,
|
||||
'sourceStorage.accessKey': config.accessKey,
|
||||
'sourceStorage.secretKey': config.secretKey,
|
||||
};
|
||||
return map[key];
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudhost-upload-'));
|
||||
mockSend.mockReset();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(uploadDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('local mode', () => {
|
||||
beforeEach(async () => {
|
||||
const module = await createModule({});
|
||||
service = module.get(SourceStorageService);
|
||||
});
|
||||
|
||||
it('stores source zip on local disk', async () => {
|
||||
const buffer = Buffer.from('zip-content');
|
||||
const storedPath = await service.putSource('user-1', 'app-1', buffer);
|
||||
|
||||
expect(service.isObjectStorage()).toBe(false);
|
||||
expect(storedPath).toBe(path.join(uploadDir, 'user-1', 'app-1', 'source.zip'));
|
||||
expect(fs.readFileSync(storedPath, 'utf8')).toBe('zip-content');
|
||||
});
|
||||
|
||||
it('checks existence and size for local files', async () => {
|
||||
const storedPath = await service.putSource('user-1', 'app-1', Buffer.from('abc'));
|
||||
|
||||
expect(await service.exists(storedPath)).toBe(true);
|
||||
expect(await service.getSize(storedPath)).toBe(3);
|
||||
});
|
||||
|
||||
it('deletes local upload directory', async () => {
|
||||
const storedPath = await service.putSource('user-1', 'app-1', Buffer.from('abc'));
|
||||
expect(fs.existsSync(storedPath)).toBe(true);
|
||||
|
||||
await service.deleteSource('user-1', 'app-1', storedPath);
|
||||
expect(fs.existsSync(path.dirname(storedPath))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('object storage mode', () => {
|
||||
beforeEach(async () => {
|
||||
const module = await createModule({
|
||||
endpoint: 'http://rgw.local:80',
|
||||
region: 'us-east-1',
|
||||
bucket: 'app-sources',
|
||||
accessKey: 'access',
|
||||
secretKey: 'secret',
|
||||
});
|
||||
service = module.get(SourceStorageService);
|
||||
});
|
||||
|
||||
it('uploads source zip to S3', async () => {
|
||||
mockSend.mockResolvedValue({});
|
||||
const buffer = Buffer.from('zip-content');
|
||||
|
||||
const key = await service.putSource('user-1', 'app-1', buffer);
|
||||
|
||||
expect(service.isObjectStorage()).toBe(true);
|
||||
expect(key).toBe('user-1/app-1/source.zip');
|
||||
expect(PutObjectCommand).toHaveBeenCalledWith({
|
||||
Bucket: 'app-sources',
|
||||
Key: 'user-1/app-1/source.zip',
|
||||
Body: buffer,
|
||||
ContentType: 'application/zip',
|
||||
});
|
||||
expect(mockSend).toHaveBeenCalled();
|
||||
expect(S3Client).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
endpoint: 'http://rgw.local:80',
|
||||
forcePathStyle: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('treats object keys as non-local paths', () => {
|
||||
expect(service.isLocalPath('user-1/app-1/source.zip')).toBe(false);
|
||||
expect(service.isLocalPath(path.join(uploadDir, 'user-1/app-1/source.zip'))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
GetObjectCommand,
|
||||
HeadObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
@Injectable()
|
||||
export class SourceStorageService {
|
||||
private readonly logger = new Logger(SourceStorageService.name);
|
||||
private readonly s3Client: S3Client | null;
|
||||
private readonly bucket: string | undefined;
|
||||
private readonly uploadDir: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const endpoint = this.configService.get<string>('sourceStorage.endpoint');
|
||||
const region = this.configService.get<string>('sourceStorage.region') || 'us-east-1';
|
||||
const accessKey = this.configService.get<string>('sourceStorage.accessKey');
|
||||
const secretKey = this.configService.get<string>('sourceStorage.secretKey');
|
||||
this.bucket = this.configService.get<string>('sourceStorage.bucket');
|
||||
this.uploadDir = this.configService.get<string>('platform.uploadDir') || './uploads';
|
||||
|
||||
if (endpoint && this.bucket && accessKey && secretKey) {
|
||||
this.s3Client = new S3Client({
|
||||
endpoint,
|
||||
region,
|
||||
credentials: { accessKeyId: accessKey, secretAccessKey: secretKey },
|
||||
forcePathStyle: true,
|
||||
});
|
||||
this.logger.log(`Object storage enabled (bucket=${this.bucket})`);
|
||||
} else {
|
||||
this.s3Client = null;
|
||||
}
|
||||
}
|
||||
|
||||
isObjectStorage(): boolean {
|
||||
return this.s3Client !== null;
|
||||
}
|
||||
|
||||
sourceKey(userId: string, appId: string, filename = 'source.zip'): string {
|
||||
return `${userId}/${appId}/${filename}`;
|
||||
}
|
||||
|
||||
isLocalPath(codePath: string): boolean {
|
||||
if (!codePath) return false;
|
||||
if (path.isAbsolute(codePath)) return true;
|
||||
if (codePath.startsWith('./') || codePath.startsWith('../')) return true;
|
||||
const normalizedUploadDir = path.resolve(this.uploadDir);
|
||||
return path.resolve(codePath).startsWith(normalizedUploadDir);
|
||||
}
|
||||
|
||||
localPath(userId: string, appId: string, filename = 'source.zip'): string {
|
||||
return path.join(this.uploadDir, userId, appId, filename);
|
||||
}
|
||||
|
||||
async putSource(userId: string, appId: string, buffer: Buffer): Promise<string> {
|
||||
if (this.isObjectStorage()) {
|
||||
const key = this.sourceKey(userId, appId);
|
||||
await this.s3Client!.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: this.bucket,
|
||||
Key: key,
|
||||
Body: buffer,
|
||||
ContentType: 'application/zip',
|
||||
}),
|
||||
);
|
||||
this.logger.log(`Uploaded source to S3 → s3://${this.bucket}/${key} (${(buffer.length / 1024).toFixed(1)} KB)`);
|
||||
return key;
|
||||
}
|
||||
|
||||
const zipPath = this.localPath(userId, appId);
|
||||
fs.mkdirSync(path.dirname(zipPath), { recursive: true });
|
||||
fs.writeFileSync(zipPath, buffer);
|
||||
this.logger.log(`Uploaded source to local → ${zipPath} (${(buffer.length / 1024).toFixed(1)} KB)`);
|
||||
return zipPath;
|
||||
}
|
||||
|
||||
async exists(codePath: string): Promise<boolean> {
|
||||
if (!codePath) return false;
|
||||
if (this.isLocalPath(codePath)) {
|
||||
return fs.existsSync(codePath);
|
||||
}
|
||||
if (!this.s3Client) return false;
|
||||
try {
|
||||
await this.s3Client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: codePath }));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getSize(codePath: string): Promise<number> {
|
||||
if (this.isLocalPath(codePath)) {
|
||||
return fs.statSync(codePath).size;
|
||||
}
|
||||
const head = await this.s3Client!.send(new HeadObjectCommand({ Bucket: this.bucket, Key: codePath }));
|
||||
return head.ContentLength ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a local filesystem path for reading the archive.
|
||||
* For S3 keys, downloads to a temp file and returns { path, cleanup }.
|
||||
*/
|
||||
async materializeToTempFile(codePath: string): Promise<{ path: string; cleanup: () => void }> {
|
||||
if (this.isLocalPath(codePath)) {
|
||||
if (!fs.existsSync(codePath)) {
|
||||
throw new Error(`Source file not found: ${codePath}`);
|
||||
}
|
||||
return { path: codePath, cleanup: () => {} };
|
||||
}
|
||||
|
||||
const response = await this.s3Client!.send(
|
||||
new GetObjectCommand({ Bucket: this.bucket, Key: codePath }),
|
||||
);
|
||||
const tempPath = path.join(
|
||||
os.tmpdir(),
|
||||
`cloudhost-source-${Date.now()}-${path.basename(codePath)}`,
|
||||
);
|
||||
const body = response.Body;
|
||||
if (!body || typeof (body as Readable).pipe !== 'function') {
|
||||
throw new Error(`Empty S3 response for key ${codePath}`);
|
||||
}
|
||||
await pipeline(body as Readable, fs.createWriteStream(tempPath));
|
||||
return {
|
||||
path: tempPath,
|
||||
cleanup: () => {
|
||||
try {
|
||||
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
||||
} catch {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async deleteSource(userId: string, appId: string, codePath?: string | null): Promise<void> {
|
||||
if (this.isObjectStorage()) {
|
||||
const key = codePath && !this.isLocalPath(codePath) ? codePath : this.sourceKey(userId, appId);
|
||||
try {
|
||||
await this.s3Client!.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
||||
this.logger.log(`Deleted S3 source → s3://${this.bucket}/${key}`);
|
||||
} catch (e: any) {
|
||||
this.logger.warn(`Failed to delete S3 source ${key}: ${e.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const appDir = path.join(this.uploadDir, userId, appId);
|
||||
if (fs.existsSync(appDir)) {
|
||||
fs.rmSync(appDir, { recursive: true, force: true });
|
||||
this.logger.log(`Deleted local upload directory: ${appDir}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { SourceStorageService } from './source-storage.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [SourceStorageService],
|
||||
exports: [SourceStorageService],
|
||||
})
|
||||
export class StorageModule {}
|
||||
+3
-1
@@ -3,7 +3,9 @@ FROM node:24-alpine AS deps
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
# lockfile may reference npmmirror; use npmjs.org inside the image build
|
||||
RUN sed -i 's|https://registry.npmmirror.com|https://registry.npmjs.org|g' package-lock.json \
|
||||
&& npm ci
|
||||
|
||||
# ---- Stage 2: Build ----
|
||||
FROM node:24-alpine AS builder
|
||||
|
||||
Generated
+25
-21
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "cloudhost-frontend",
|
||||
"name": "abrban-frontend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cloudhost-frontend",
|
||||
"name": "abrban-frontend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^10.7.7",
|
||||
@@ -86,7 +86,6 @@
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
@@ -311,6 +310,28 @@
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
@@ -1764,7 +1785,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.1.tgz",
|
||||
"integrity": "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8",
|
||||
"@types/webxr": "*",
|
||||
@@ -2565,7 +2585,6 @@
|
||||
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
@@ -2594,7 +2613,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
||||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -2629,7 +2647,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz",
|
||||
"integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
@@ -2697,7 +2714,6 @@
|
||||
"integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.61.0",
|
||||
"@typescript-eslint/types": "8.61.0",
|
||||
@@ -3434,7 +3450,6 @@
|
||||
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3857,7 +3872,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@@ -4664,7 +4678,6 @@
|
||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -4850,7 +4863,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -7376,7 +7388,6 @@
|
||||
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.1.tgz",
|
||||
"integrity": "sha512-R2dG2zy+BAx3USl5EHw+PvnrlbT5PKnZVp3se0HCR0pWH8WQdh742yNG4YWOsq6c0bFpffk0Gd2RqPeoP/wKng==",
|
||||
"license": "Zlib",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.168.0 < 0.185.0"
|
||||
}
|
||||
@@ -7474,7 +7485,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7484,7 +7494,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -8359,8 +8368,7 @@
|
||||
"version": "0.184.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
|
||||
"integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/three-mesh-bvh": {
|
||||
"version": "0.8.3",
|
||||
@@ -8449,7 +8457,6 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -8709,7 +8716,6 @@
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -8997,7 +9003,6 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -9259,7 +9264,6 @@
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "cloudhost-frontend",
|
||||
"name": "abrban-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1062,7 +1062,7 @@ export default function AppDetailPage() {
|
||||
// The host the app is actually reachable on right now: the verified custom
|
||||
// domain when present, otherwise the platform-assigned subdomain. Single source
|
||||
// of truth so every place that shows "the app's domain" stays in sync.
|
||||
const platformHost = `${app.subdomain}.${domainInfo?.platformDomain || 'apps.cloudhost.ir'}`;
|
||||
const platformHost = `${app.subdomain}.${domainInfo?.platformDomain || 'apps.abrban.com'}`;
|
||||
const currentDomain =
|
||||
app.customDomain && app.customDomainStatus === 'verified' ? app.customDomain : platformHost;
|
||||
|
||||
@@ -1735,7 +1735,7 @@ export default function AppDetailPage() {
|
||||
<div className="pl-4 rtl:pl-0 rtl:pr-4 space-y-1">
|
||||
<p className="text-xs font-mono bg-gray-50 rounded px-2 py-1">{ad.nameHost}<strong>@</strong> {ad.orText} <strong>www</strong></p>
|
||||
<p className="text-xs font-mono bg-gray-50 rounded px-2 py-1">{ad.typeColon}<strong>CNAME</strong></p>
|
||||
<p className="text-xs font-mono bg-gray-50 rounded px-2 py-1">{ad.valueColon}<strong>{domainInfo?.fullPlatformUrl || `${app.subdomain}.apps.cloudhost.ir`}</strong></p>
|
||||
<p className="text-xs font-mono bg-gray-50 rounded px-2 py-1">{ad.valueColon}<strong>{domainInfo?.fullPlatformUrl || `${app.subdomain}.apps.abrban.com`}</strong></p>
|
||||
</div>
|
||||
<p>{ad.dnsStep4a}<code className="bg-gray-100 px-1 rounded">www</code>{ad.dnsStep4b}</p>
|
||||
<p>{ad.dnsStep5}</p>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# GitOps stack for abrban.com
|
||||
|
||||
## DNS (A record → cluster IP `78.157.39.52`)
|
||||
|
||||
| Host | Purpose |
|
||||
|------|---------|
|
||||
| `abrban.com` | Landing / frontend |
|
||||
| `panel.abrban.com` | Authenticated panel |
|
||||
| `api.abrban.com` | Backend API |
|
||||
| `registry.abrban.com` | Harbor |
|
||||
| `git.abrban.com` | Gitea |
|
||||
| `argocd.abrban.com` | Argo CD |
|
||||
|
||||
## Harbor proxy-cache (بدون mirror دستی)
|
||||
|
||||
Harbor ایمیجهای upstream را on-demand میکشد و cache میکند:
|
||||
|
||||
| پروژه Harbor | upstream | مثال |
|
||||
|--------------|----------|------|
|
||||
| `proxy-dockerhub` | docker.io | `registry.abrban.com/proxy-dockerhub/alpine/git:2.43.0` |
|
||||
| `proxy-quay` | quay.io | `registry.abrban.com/proxy-quay/argoproj/argocd:v3.4.4` |
|
||||
| `proxy-k8s` | registry.k8s.io | CSI sidecarها |
|
||||
| `proxy-gitea` | docker.gitea.com | `registry.abrban.com/proxy-gitea/gitea:1.26.1-rootless` |
|
||||
| `proxy-gcr` | gcr.io | `registry.abrban.com/proxy-gcr/kaniko-project/executor:v1.23.2` |
|
||||
| `abrban` | — | ایمیجهای ساختهشده (backend/frontend) |
|
||||
|
||||
اولین pull هر ایمیج کمی طول میکشد (Harbor از upstream میکشد). **نیازی به Job skopeo جداگانه نیست.**
|
||||
|
||||
k3s باید `registry.abrban.com` را به Harbor داخلی route کند:
|
||||
|
||||
```bash
|
||||
./scripts/apply-k3s-registries.sh
|
||||
```
|
||||
|
||||
## Install order
|
||||
|
||||
```bash
|
||||
# 1. k3s → Harbor داخلی (بدون Traefik timeout)
|
||||
./scripts/apply-k3s-registries.sh
|
||||
|
||||
# 2. Argo CD
|
||||
helm upgrade --install argocd argo/argo-cd -n argocd --create-namespace \
|
||||
-f gitops/argocd/values-bootstrap.yaml --timeout 15m --wait
|
||||
|
||||
# 3. Gitea (ایمیجها از Harbor proxy-gitea)
|
||||
helm upgrade --install gitea gitea-charts/gitea -n gitea --create-namespace \
|
||||
-f gitops/gitea/values.yaml --timeout 15m --wait
|
||||
|
||||
# 4. TLS + registry secrets در namespaceهای argocd/gitea/cloudhost-builds
|
||||
for ns in argocd gitea cloudhost-builds; do
|
||||
kubectl -n cloudhost get secret abrban-wildcard-tls -o yaml | sed "s/namespace: cloudhost/namespace: ${ns}/" | kubectl apply -f -
|
||||
kubectl -n cloudhost get secret registry-pull-secret -o yaml | sed "s/namespace: cloudhost/namespace: ${ns}/" | kubectl apply -f -
|
||||
done
|
||||
|
||||
# 5. git push سورس به Gitea (نه docker push)
|
||||
# git remote add gitea https://git.abrban.com/abrban/cloud-host.git
|
||||
# git push gitea main
|
||||
|
||||
# 6. اولین build در کلاستر (Kaniko → abrban/)
|
||||
./scripts/trigger-platform-build.sh
|
||||
|
||||
# 7. Deploy
|
||||
./scripts/gitops-deploy.sh
|
||||
|
||||
# 8. Gitea Actions runner
|
||||
kubectl apply -f gitops/gitea/act-runner.yaml
|
||||
|
||||
# 9. Argo CD Application
|
||||
kubectl apply -f gitops/argocd/application-platform.yaml
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
Gitea Actions: [.gitea/workflows/build-deploy.yaml](../.gitea/workflows/build-deploy.yaml)
|
||||
|
||||
Push به `main` → Kaniko (از Harbor proxy) → push به `abrban/` → ArgoCD sync.
|
||||
@@ -0,0 +1,25 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: abrban-platform
|
||||
namespace: argocd
|
||||
finalizers:
|
||||
- resources-finalizer.argocd.argoproj.io
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: https://git.abrban.com/abrban/cloud-host.git
|
||||
targetRevision: main
|
||||
path: backend/helm/cloudhost-platform
|
||||
helm:
|
||||
valueFiles:
|
||||
- ../../../gitops/platform/values-abrban.yaml
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: cloudhost
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=false
|
||||
@@ -0,0 +1,32 @@
|
||||
# Bootstrap install — uses images already cached on node (no Harbor push required)
|
||||
configs:
|
||||
params:
|
||||
server.insecure: true
|
||||
|
||||
global:
|
||||
image:
|
||||
repository: quay.io/argoproj/argocd
|
||||
tag: v3.4.4
|
||||
|
||||
redis:
|
||||
image:
|
||||
repository: ecr-public.aws.com/docker/library/redis
|
||||
tag: 8.2.3-alpine
|
||||
|
||||
dex:
|
||||
image:
|
||||
repository: ghcr.io/dexidp/dex
|
||||
tag: v2.45.1
|
||||
|
||||
server:
|
||||
ingress:
|
||||
enabled: true
|
||||
ingressClassName: traefik
|
||||
hostname: argocd.abrban.com
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
tls: true
|
||||
extraTls:
|
||||
- secretName: abrban-wildcard-tls
|
||||
hosts:
|
||||
- argocd.abrban.com
|
||||
@@ -0,0 +1,33 @@
|
||||
global:
|
||||
image:
|
||||
repository: registry.abrban.com/proxy-quay/argoproj/argocd
|
||||
tag: v3.4.4
|
||||
imagePullSecrets:
|
||||
- name: registry-pull-secret
|
||||
|
||||
redis:
|
||||
image:
|
||||
repository: registry.abrban.com/proxy-dockerhub/library/redis
|
||||
tag: 8.2.3-alpine
|
||||
|
||||
dex:
|
||||
image:
|
||||
repository: registry.abrban.com/proxy-dockerhub/dexidp/dex
|
||||
tag: v2.45.1
|
||||
|
||||
configs:
|
||||
params:
|
||||
server.insecure: true
|
||||
|
||||
server:
|
||||
ingress:
|
||||
enabled: true
|
||||
ingressClassName: traefik
|
||||
hostname: argocd.abrban.com
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
tls: true
|
||||
extraTls:
|
||||
- secretName: abrban-wildcard-tls
|
||||
hosts:
|
||||
- argocd.abrban.com
|
||||
@@ -0,0 +1,57 @@
|
||||
# Gitea Actions runner — host mode + Kaniko (no docker.sock; k3s uses containerd)
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: gitea-act-runner-token
|
||||
namespace: gitea
|
||||
type: Opaque
|
||||
stringData:
|
||||
token: "nL63VkZEyqpCNFdF3AMM9wzQLdLlATUvXSe5Tj0R"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gitea-act-runner
|
||||
namespace: gitea
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gitea-act-runner
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gitea-act-runner
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: registry-pull-secret
|
||||
initContainers:
|
||||
- name: kaniko-bin
|
||||
image: registry.abrban.com/abrban/kaniko-executor:v1.23.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
command: ["sh", "-c", "cp /kaniko/executor /kaniko-bin/executor && chmod +x /kaniko-bin/executor"]
|
||||
volumeMounts:
|
||||
- name: kaniko-bin
|
||||
mountPath: /kaniko-bin
|
||||
containers:
|
||||
- name: runner
|
||||
image: registry.abrban.com/abrban/act-runner:0.2.11
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: GITEA_INSTANCE_URL
|
||||
value: https://git.abrban.com
|
||||
- name: GITEA_RUNNER_REGISTRATION_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: gitea-act-runner-token
|
||||
key: token
|
||||
- name: GITEA_RUNNER_NAME
|
||||
value: k8s-abr-runner
|
||||
- name: GITEA_RUNNER_LABELS
|
||||
value: abrban-kaniko:host
|
||||
volumeMounts:
|
||||
- name: kaniko-bin
|
||||
mountPath: /kaniko
|
||||
volumes:
|
||||
- name: kaniko-bin
|
||||
emptyDir: {}
|
||||
@@ -0,0 +1,55 @@
|
||||
global:
|
||||
imagePullSecrets:
|
||||
- registry-pull-secret
|
||||
storageClass: local-path
|
||||
|
||||
# Local copy in abrban/ (seeded by copy-gitea-image job) — kubelet pulls without proxy-cache
|
||||
image:
|
||||
registry: registry.abrban.com
|
||||
repository: abrban/gitea
|
||||
tag: 1.26.1
|
||||
rootless: true
|
||||
|
||||
gitea:
|
||||
config:
|
||||
database:
|
||||
DB_TYPE: sqlite3
|
||||
server:
|
||||
DOMAIN: git.abrban.com
|
||||
ROOT_URL: https://git.abrban.com/
|
||||
SSH_DOMAIN: git.abrban.com
|
||||
actions:
|
||||
ENABLED: true
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: traefik
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
hosts:
|
||||
- host: git.abrban.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: abrban-wildcard-tls
|
||||
hosts:
|
||||
- git.abrban.com
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
storageClass: local-path
|
||||
size: 10Gi
|
||||
|
||||
# SQLite — no extra DB images to pull
|
||||
postgresql:
|
||||
enabled: false
|
||||
|
||||
valkey-cluster:
|
||||
enabled: false
|
||||
|
||||
valkey:
|
||||
enabled: false
|
||||
|
||||
postgresql-ha:
|
||||
enabled: false
|
||||
@@ -0,0 +1,60 @@
|
||||
# In-cluster platform build (Kaniko → Harbor abrban/). Apply via scripts/trigger-platform-build.sh
|
||||
# Base images pulled via Harbor proxy-cache — no separate mirror job.
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: build-platform-images
|
||||
namespace: cloudhost-builds
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 3600
|
||||
backoffLimit: 1
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
imagePullSecrets:
|
||||
- name: registry-pull-secret
|
||||
initContainers:
|
||||
- name: git-clone
|
||||
image: registry.abrban.com/proxy-dockerhub/alpine/git:2.43.0
|
||||
env:
|
||||
- name: GIT_REPO
|
||||
value: http://gitea-http.gitea.svc.cluster.local:3000/abrban/cloud-host.git
|
||||
- name: GIT_REF
|
||||
value: main
|
||||
command:
|
||||
- sh
|
||||
- -ec
|
||||
- |
|
||||
git clone --depth=1 --branch "${GIT_REF}" "${GIT_REPO}" /workspace
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
containers:
|
||||
- name: kaniko
|
||||
image: registry.abrban.com/proxy-gcr/kaniko-project/executor:v1.23.2
|
||||
env:
|
||||
- name: IMAGE_TAG
|
||||
value: bootstrap
|
||||
command:
|
||||
- sh
|
||||
- -ec
|
||||
- |
|
||||
set -eux
|
||||
REG="harbor-registry.cloudhost.svc.cluster.local:5000/abrban"
|
||||
/kaniko/executor \
|
||||
--dockerfile=/workspace/backend/Dockerfile \
|
||||
--context=dir:///workspace/backend \
|
||||
--destination="${REG}/cloudhost-backend:${IMAGE_TAG}" \
|
||||
--insecure --skip-tls-verify
|
||||
/kaniko/executor \
|
||||
--dockerfile=/workspace/frontend/Dockerfile \
|
||||
--context=dir:///workspace/frontend \
|
||||
--build-arg=NEXT_PUBLIC_API_URL=https://api.abrban.com \
|
||||
--destination="${REG}/cloudhost-frontend:${IMAGE_TAG}" \
|
||||
--insecure --skip-tls-verify
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
volumes:
|
||||
- name: workspace
|
||||
emptyDir: {}
|
||||
@@ -0,0 +1,37 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: image-import
|
||||
namespace: cloudhost
|
||||
spec:
|
||||
nodeName: abr
|
||||
restartPolicy: Never
|
||||
hostNetwork: true
|
||||
containers:
|
||||
- name: import
|
||||
image: quay.io/skopeo/stable:latest
|
||||
command:
|
||||
- sh
|
||||
- -ec
|
||||
- |
|
||||
sleep 3600
|
||||
securityContext:
|
||||
privileged: true
|
||||
volumeMounts:
|
||||
- name: containerd-sock
|
||||
mountPath: /run/containerd/containerd.sock
|
||||
- name: containerd-sock-k3s
|
||||
mountPath: /run/k3s/containerd/containerd.sock
|
||||
- name: import-dir
|
||||
mountPath: /import
|
||||
volumes:
|
||||
- name: containerd-sock
|
||||
hostPath:
|
||||
path: /run/k3s/containerd/containerd.sock
|
||||
type: Socket
|
||||
- name: containerd-sock-k3s
|
||||
hostPath:
|
||||
path: /run/k3s/containerd/containerd.sock
|
||||
type: Socket
|
||||
- name: import-dir
|
||||
emptyDir: {}
|
||||
@@ -0,0 +1,33 @@
|
||||
# Bootstrap: copy act_runner + kaniko into abrban/ (kubelet cannot use proxy-cache reliably)
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: seed-ci-images
|
||||
namespace: cloudhost
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 3600
|
||||
backoffLimit: 2
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
imagePullSecrets:
|
||||
- name: registry-pull-secret
|
||||
containers:
|
||||
- name: skopeo
|
||||
image: registry.abrban.com/proxy-quay/skopeo/stable:latest
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: registry-egress-proxy
|
||||
command:
|
||||
- sh
|
||||
- -ec
|
||||
- |
|
||||
set -eux
|
||||
DEST="docker://harbor-registry.cloudhost.svc.cluster.local:5000/abrban"
|
||||
skopeo copy --dest-tls-verify=false \
|
||||
docker://docker.gitea.com/gitea/act_runner:0.2.11 \
|
||||
"${DEST}/act-runner:0.2.11"
|
||||
skopeo copy --dest-tls-verify=false \
|
||||
docker://gcr.io/kaniko-project/executor:v1.23.2 \
|
||||
"${DEST}/kaniko-executor:v1.23.2"
|
||||
echo SEED_OK
|
||||
@@ -0,0 +1,15 @@
|
||||
# k3s containerd registry config — apply on each node at /etc/rancher/k3s/registries.yaml
|
||||
# Proxy-cache only works through harbor-core (not harbor-registry or Traefik /v2/ alone).
|
||||
#
|
||||
# Apply: ./scripts/apply-k3s-registries.sh
|
||||
|
||||
mirrors:
|
||||
registry.abrban.com:
|
||||
endpoint:
|
||||
- http://harbor-core.cloudhost.svc.cluster.local # use ClusterIP on single-node (see script)
|
||||
|
||||
configs:
|
||||
registry.abrban.com:
|
||||
auth:
|
||||
username: harbor_registry_user
|
||||
password: REPLACE_WITH_REGISTRY_CREDENTIAL_PASSWORD
|
||||
@@ -0,0 +1,56 @@
|
||||
# Production values for abrban.com — used by ArgoCD / Gitea Actions GitOps
|
||||
namespace: cloudhost
|
||||
createNamespace: false
|
||||
|
||||
global:
|
||||
storageClass: local-path
|
||||
|
||||
images:
|
||||
backend:
|
||||
repository: registry.abrban.com/abrban/cloudhost-backend
|
||||
tag: "1.0.0"
|
||||
pullPolicy: Always
|
||||
frontend:
|
||||
repository: registry.abrban.com/abrban/cloudhost-frontend
|
||||
tag: "1.0.0"
|
||||
pullPolicy: Always
|
||||
|
||||
backend:
|
||||
imagePullSecrets:
|
||||
- name: registry-pull-secret
|
||||
sourceStorage:
|
||||
enabled: true
|
||||
existingSecret: ceph-app-sources-credentials
|
||||
env:
|
||||
NODE_ENV: production
|
||||
PORT: "4000"
|
||||
PLATFORM_DOMAIN: apps.abrban.com
|
||||
REGISTRY_URL: registry.abrban.com
|
||||
REGISTRY_PULL_URL: registry.abrban.com
|
||||
BUILD_NAMESPACE: cloudhost-builds
|
||||
BUILD_SERVICE_ACCOUNT: kaniko-builder
|
||||
UPLOAD_DIR: /app/uploads
|
||||
PLATFORM_CREATE_STORAGE_CLASS: "false"
|
||||
PLATFORM_STORAGE_CLASS: rook-ceph-block
|
||||
PLATFORM_STORAGE_PROVISIONER: rook-ceph.rbd.csi.ceph.com
|
||||
ELASTICSEARCH_HOST: elasticsearch.logging.svc.cluster.local
|
||||
ELASTICSEARCH_AUTO_PORT_FORWARD: "false"
|
||||
|
||||
frontend:
|
||||
imagePullSecrets:
|
||||
- name: registry-pull-secret
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: traefik
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
frontend:
|
||||
host: abrban.com
|
||||
panel:
|
||||
host: panel.abrban.com
|
||||
api:
|
||||
host: api.abrban.com
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: abrban-wildcard-tls
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: long-timeout
|
||||
namespace: cloudhost
|
||||
spec:
|
||||
buffering:
|
||||
maxRequestBodyBytes: 0
|
||||
memRequestBodyBytes: 0
|
||||
maxResponseBodyBytes: 0
|
||||
memResponseBodyBytes: 0
|
||||
retryExpression: "IsNetworkError() && Attempts() < 3"
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Route registry.abrban.com pulls to harbor-core (HTTP) for proxy-cache support.
|
||||
set -euo pipefail
|
||||
|
||||
NS="${NS:-kube-system}"
|
||||
NODE="${NODE:-abr}"
|
||||
HARBOR_CORE_IP="${HARBOR_CORE_IP:-$(kubectl -n cloudhost get svc harbor-core -o jsonpath='{.spec.clusterIP}')}"
|
||||
REG_USER="${REG_USER:-harbor_registry_user}"
|
||||
REG_PASS="${REG_PASS:-$(kubectl -n cloudhost get secret harbor-core -o jsonpath='{.data.REGISTRY_CREDENTIAL_PASSWORD}' | base64 -d)}"
|
||||
|
||||
kubectl -n "${NS}" delete pod k3s-registries-setup --ignore-not-found
|
||||
|
||||
kubectl -n "${NS}" run k3s-registries-setup \
|
||||
--image=rancher/mirrored-library-busybox:1.36.1 \
|
||||
--restart=Never \
|
||||
--overrides="$(cat <<EOF
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"spec": {
|
||||
"nodeName": "${NODE}",
|
||||
"hostNetwork": true,
|
||||
"containers": [{
|
||||
"name": "setup",
|
||||
"image": "rancher/mirrored-library-busybox:1.36.1",
|
||||
"securityContext": {"privileged": true},
|
||||
"command": ["sh", "-ec", "mkdir -p /host/etc/rancher/k3s && cat > /host/etc/rancher/k3s/registries.yaml <<'REGEOF'\nmirrors:\n registry.abrban.com:\n endpoint:\n - http://${HARBOR_CORE_IP}\n \\\"registry.cloudhost-builds.svc.cluster.local:5000\\\":\n endpoint:\n - \\\"http://127.0.0.1:30500\\\"\nconfigs:\n registry.abrban.com:\n auth:\n username: ${REG_USER}\n password: ${REG_PASS}\n \\\"${HARBOR_CORE_IP}\\\":\n auth:\n username: ${REG_USER}\n password: ${REG_PASS}\n \\\"registry.cloudhost-builds.svc.cluster.local:5000\\\":\n auth:\n username: admin\n password: \\\"\\\"\n \\\"127.0.0.1:30500\\\":\n auth:\n username: admin\n password: \\\"\\\"\nREGEOF\nnsenter -t 1 -m -u -n -i -- systemctl restart k3s 2>/dev/null || true\necho k3s-restarted\nsleep 30"],
|
||||
"volumeMounts": [{"name": "host", "mountPath": "/host"}]
|
||||
}],
|
||||
"volumes": [{"name": "host", "hostPath": {"path": "/"}}]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)"
|
||||
|
||||
kubectl -n "${NS}" wait --for=condition=Ready pod/k3s-registries-setup --timeout=180s || true
|
||||
kubectl -n "${NS}" logs k3s-registries-setup
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deploy platform via Helm only — images must already be in Harbor (built by Gitea Actions / Kaniko).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
NAMESPACE="${NAMESPACE:-cloudhost}"
|
||||
RELEASE="${RELEASE:-cloudhost}"
|
||||
VALUES="${VALUES:-${ROOT}/gitops/platform/values-abrban.yaml}"
|
||||
TAG="${TAG:-}"
|
||||
|
||||
if [[ -z "${TAG}" ]]; then
|
||||
TAG="$(grep -E '^\s+tag:' "${VALUES}" | head -1 | sed 's/.*tag: *"\?\([^"]*\)"\?.*/\1/')"
|
||||
fi
|
||||
|
||||
if [[ -z "${TAG}" || "${TAG}" == "1.0.0" ]]; then
|
||||
echo "ERROR: No image tag set. Build in-cluster first:" >&2
|
||||
echo " ./scripts/trigger-platform-build.sh" >&2
|
||||
echo "Or set TAG=... after CI has pushed images." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Helm upgrade ${RELEASE} (tag=${TAG})"
|
||||
helm upgrade --install "${RELEASE}" "${ROOT}/backend/helm/cloudhost-platform" \
|
||||
-n "${NAMESPACE}" \
|
||||
-f "${VALUES}" \
|
||||
--set createNamespace=false \
|
||||
--set global.storageClass=local-path \
|
||||
--set images.backend.tag="${TAG}" \
|
||||
--set images.frontend.tag="${TAG}" \
|
||||
--timeout 15m \
|
||||
--wait
|
||||
|
||||
kubectl -n "${NAMESPACE}" rollout status deploy/cloudhost-backend --timeout=300s
|
||||
kubectl -n "${NAMESPACE}" rollout status deploy/cloudhost-frontend --timeout=300s
|
||||
|
||||
echo "==> Done (tag=${TAG})"
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# DEPRECATED — use Harbor proxy-cache instead (see gitops/README.md).
|
||||
# Harbor projects: proxy-dockerhub, proxy-quay, proxy-k8s, proxy-gitea, proxy-gcr
|
||||
echo "ERROR: Do not use this script. Harbor proxy-cache pulls upstream images on demand." >&2
|
||||
echo "See gitops/README.md and RUNBOOK-HARBOR.fa.md" >&2
|
||||
exit 1
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# Trigger in-cluster Kaniko build → Harbor. No local docker build/push.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
NAMESPACE="${BUILD_NAMESPACE:-cloudhost-builds}"
|
||||
JOB_NAME="${JOB_NAME:-build-platform-images}"
|
||||
GIT_REPO="${GIT_REPO:-http://gitea-http.gitea.svc.cluster.local:3000/abrban/cloud-host.git}"
|
||||
GIT_REF="${GIT_REF:-main}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-$(date +%Y%m%d-%H%M)}"
|
||||
|
||||
echo "==> Applying Kaniko build job (tag=${IMAGE_TAG}, ref=${GIT_REF})"
|
||||
kubectl -n "${NAMESPACE}" delete job "${JOB_NAME}" --ignore-not-found
|
||||
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: ${JOB_NAME}
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 3600
|
||||
backoffLimit: 1
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
imagePullSecrets:
|
||||
- name: registry-pull-secret
|
||||
initContainers:
|
||||
- name: git-clone
|
||||
image: registry.abrban.com/proxy-dockerhub/alpine/git:2.43.0
|
||||
command:
|
||||
- sh
|
||||
- -ec
|
||||
- |
|
||||
git clone --depth=1 --branch "${GIT_REF}" "${GIT_REPO}" /workspace
|
||||
ls -la /workspace
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
containers:
|
||||
- name: kaniko
|
||||
image: registry.abrban.com/proxy-gcr/kaniko-project/executor:v1.23.2
|
||||
command:
|
||||
- sh
|
||||
- -ec
|
||||
- |
|
||||
set -eux
|
||||
REG="harbor-registry.cloudhost.svc.cluster.local:5000/abrban"
|
||||
/kaniko/executor \
|
||||
--dockerfile=/workspace/backend/Dockerfile \
|
||||
--context=dir:///workspace/backend \
|
||||
--destination="${REG}/cloudhost-backend:${IMAGE_TAG}" \
|
||||
--insecure --skip-tls-verify
|
||||
/kaniko/executor \
|
||||
--dockerfile=/workspace/frontend/Dockerfile \
|
||||
--context=dir:///workspace/frontend \
|
||||
--build-arg=NEXT_PUBLIC_API_URL=https://api.abrban.com \
|
||||
--destination="${REG}/cloudhost-frontend:${IMAGE_TAG}" \
|
||||
--insecure --skip-tls-verify
|
||||
echo "BUILT_TAG=${IMAGE_TAG}"
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
volumes:
|
||||
- name: workspace
|
||||
emptyDir: {}
|
||||
EOF
|
||||
|
||||
echo "==> Waiting for build job..."
|
||||
kubectl -n "${NAMESPACE}" wait --for=condition=complete "job/${JOB_NAME}" --timeout=45m
|
||||
|
||||
echo "==> Updating values-abrban.yaml tag to ${IMAGE_TAG}"
|
||||
sed -i.bak "s|tag: \".*\"|tag: \"${IMAGE_TAG}\"|g" "${ROOT}/gitops/platform/values-abrban.yaml"
|
||||
rm -f "${ROOT}/gitops/platform/values-abrban.yaml.bak"
|
||||
|
||||
echo "==> Build complete. Deploy with:"
|
||||
echo " TAG=${IMAGE_TAG} ./scripts/gitops-deploy.sh"
|
||||
Reference in New Issue
Block a user