feat(admin): add Docker and Kubernetes deployment for admin panel
This commit is contained in:
parent
208b6ed84e
commit
b8e5cbbb69
50
admin/Dockerfile
Normal file
50
admin/Dockerfile
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Stage 1: Build
|
||||||
|
# =============================================================================
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies first (layer cache optimization)
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build Next.js
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage 2: Runtime (minimal Node.js image)
|
||||||
|
# =============================================================================
|
||||||
|
FROM node:20-alpine AS runtime
|
||||||
|
|
||||||
|
# Install dumb-init for proper signal handling
|
||||||
|
RUN apk add --no-cache dumb-init
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy built artifacts from builder
|
||||||
|
COPY --from=builder /app/.next .next
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/package.json ./
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Non-root user for security
|
||||||
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
adduser -S nextjs -u 1001
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD wget -qO- http://localhost:3000/api/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
|
CMD ["node_modules/.bin/next", "start"]
|
||||||
13
admin/deploy/Chart.yaml
Normal file
13
admin/deploy/Chart.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: admin
|
||||||
|
description: GitData.AI Admin Panel (Next.js)
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "0.1.0"
|
||||||
|
keywords:
|
||||||
|
- admin
|
||||||
|
- nextjs
|
||||||
|
- gitdata
|
||||||
|
maintainers:
|
||||||
|
- name: gitdata Team
|
||||||
|
email: team@c.dev
|
||||||
15
admin/deploy/templates/_helpers.tpl
Normal file
15
admin/deploy/templates/_helpers.tpl
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{{/* =============================================================================
|
||||||
|
Common helpers
|
||||||
|
============================================================================= */}}
|
||||||
|
|
||||||
|
{{- define "admin.fullname" -}}
|
||||||
|
{{- .Release.Name -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "admin.namespace" -}}
|
||||||
|
{{- .Values.namespace | default .Release.Namespace -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "admin.image" -}}
|
||||||
|
{{- printf "%s/%s:%s" .Values.image.registry .Values.admin.image.repository .Values.admin.image.tag -}}
|
||||||
|
{{- end -}}
|
||||||
117
admin/deploy/templates/admin-deployment.yaml
Normal file
117
admin/deploy/templates/admin-deployment.yaml
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
{{- if .Values.admin.enabled -}}
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "admin.fullname" . }}-admin
|
||||||
|
namespace: {{ include "admin.namespace" . }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ include "admin.fullname" . }}-admin
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.admin.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: {{ include "admin.fullname" . }}-admin
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ include "admin.fullname" . }}-admin
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
spec:
|
||||||
|
{{- if $.Values.image.pullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- range $.Values.image.pullSecrets }}
|
||||||
|
- name: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
terminationGracePeriodSeconds: 30
|
||||||
|
containers:
|
||||||
|
- name: admin
|
||||||
|
image: "{{ include "admin.image" . }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: {{ .Values.admin.service.port }}
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: NODE_ENV
|
||||||
|
value: {{ .Values.admin.config.nodeEnv | default "production" | quote }}
|
||||||
|
{{- if .Values.admin.config.appUrl }}
|
||||||
|
- name: NEXT_PUBLIC_APP_URL
|
||||||
|
value: {{ .Values.admin.config.appUrl | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.admin.config.appDomain }}
|
||||||
|
- name: NEXT_PUBLIC_APP_DOMAIN
|
||||||
|
value: {{ .Values.admin.config.appDomain | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.secrets.enabled }}
|
||||||
|
- name: {{ .Values.admin.secretKeys.databaseUrl }}
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ include "admin.fullname" . }}-secrets
|
||||||
|
key: {{ .Values.admin.secretKeys.databaseUrl }}
|
||||||
|
- name: {{ .Values.admin.secretKeys.redisUrl }}
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ include "admin.fullname" . }}-secrets
|
||||||
|
key: {{ .Values.admin.secretKeys.redisUrl }}
|
||||||
|
- name: {{ .Values.admin.secretKeys.nextAuthSecret }}
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ include "admin.fullname" . }}-secrets
|
||||||
|
key: {{ .Values.admin.secretKeys.nextAuthSecret }}
|
||||||
|
{{- end }}
|
||||||
|
{{- range .Values.admin.env }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
value: {{ .value | quote }}
|
||||||
|
{{- end }}
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: {{ .Values.admin.livenessProbe.path }}
|
||||||
|
port: {{ .Values.admin.livenessProbe.port }}
|
||||||
|
initialDelaySeconds: {{ .Values.admin.livenessProbe.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.admin.livenessProbe.periodSeconds }}
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: {{ .Values.admin.readinessProbe.path }}
|
||||||
|
port: {{ .Values.admin.readinessProbe.port }}
|
||||||
|
initialDelaySeconds: {{ .Values.admin.readinessProbe.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.admin.readinessProbe.periodSeconds }}
|
||||||
|
{{- if .Values.admin.resources }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.admin.resources | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.admin.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.admin.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.admin.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "admin.fullname" . }}-admin
|
||||||
|
namespace: {{ include "admin.namespace" . }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ include "admin.fullname" . }}-admin
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.admin.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.admin.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: {{ include "admin.fullname" . }}-admin
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
48
admin/deploy/templates/admin-ingress.yaml
Normal file
48
admin/deploy/templates/admin-ingress.yaml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{{- if and .Values.admin.enabled .Values.admin.ingress.enabled -}}
|
||||||
|
{{- $fullName := include "admin.fullname" . -}}
|
||||||
|
{{- $ns := include "admin.namespace" . -}}
|
||||||
|
{{- $hosts := .Values.admin.ingress.hosts | default list -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ $fullName }}-admin-ingress
|
||||||
|
namespace: {{ $ns }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ $fullName }}-admin
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
annotations:
|
||||||
|
{{- if .Values.admin.ingress.annotations }}
|
||||||
|
{{- toYaml .Values.admin.ingress.annotations | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if $.Values.certManager.enabled }}
|
||||||
|
cert-manager.io/cluster-issuer: {{ $.Values.certManager.clusterIssuerName }}
|
||||||
|
{{- end }}
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/enable-websocket: "true"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
{{- if and $hosts $.Values.certManager.enabled }}
|
||||||
|
tls:
|
||||||
|
{{- range $hosts }}
|
||||||
|
- hosts:
|
||||||
|
- {{ .host }}
|
||||||
|
secretName: {{ $fullName }}-admin-tls
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range $hosts }}
|
||||||
|
- host: {{ .host }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ $fullName }}-admin
|
||||||
|
port:
|
||||||
|
number: {{ $.Values.admin.service.port }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
31
admin/deploy/templates/secret.yaml
Normal file
31
admin/deploy/templates/secret.yaml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{{- /*
|
||||||
|
Bootstrap secrets for development only.
|
||||||
|
In production, use an external secret manager (Vault, SealedSecrets, External Secrets).
|
||||||
|
*/ -}}
|
||||||
|
{{- $secrets := .Values.secrets | default dict -}}
|
||||||
|
{{- if and (ne $secrets.enabled false) $secrets.enabled -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ include "admin.fullname" . }}-secrets
|
||||||
|
namespace: {{ include "admin.namespace" . }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ .Chart.Name }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
annotations:
|
||||||
|
"helm.sh/resource-policy": keep
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
{{- if $secrets.databaseUrl }}
|
||||||
|
{{ .Values.admin.secretKeys.databaseUrl }}: {{ $secrets.databaseUrl | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if $secrets.redisUrl }}
|
||||||
|
{{ .Values.admin.secretKeys.redisUrl }}: {{ $secrets.redisUrl | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if $secrets.nextAuthSecret }}
|
||||||
|
{{ .Values.admin.secretKeys.nextAuthSecret }}: {{ $secrets.nextAuthSecret | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- range $key, $value := $secrets.extra | default dict }}
|
||||||
|
{{ $key }}: {{ $value | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
44
admin/deploy/values.user.yaml.example
Normal file
44
admin/deploy/values.user.yaml.example
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# User overrides for Helm deployment
|
||||||
|
# Copy to values.user.yaml and customize
|
||||||
|
|
||||||
|
namespace: gitdataai
|
||||||
|
releaseName: admin
|
||||||
|
|
||||||
|
image:
|
||||||
|
registry: harbor.gitdata.me/gta_team
|
||||||
|
pullSecrets:
|
||||||
|
- harbor-secret
|
||||||
|
|
||||||
|
# Admin config
|
||||||
|
admin:
|
||||||
|
replicaCount: 2
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- host: admin.gitdata.ai
|
||||||
|
annotations:
|
||||||
|
# nginx.ingress.kubernetes.io/proxy-body-size: "50m"
|
||||||
|
|
||||||
|
config:
|
||||||
|
appUrl: "https://admin.gitdata.ai"
|
||||||
|
appDomain: "admin.gitdata.ai"
|
||||||
|
nodeEnv: production
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
|
||||||
|
# Secrets (bootstrap – use external secrets in production)
|
||||||
|
secrets:
|
||||||
|
enabled: true
|
||||||
|
databaseUrl: "postgresql://user:pass@host:5432/gitdata"
|
||||||
|
redisUrl: "redis://host:6379/0"
|
||||||
|
nextAuthSecret: "your-secret-here"
|
||||||
|
|
||||||
|
# Or use external secrets
|
||||||
|
# externalSecrets:
|
||||||
|
# storeName: vault-backend
|
||||||
78
admin/deploy/values.yaml
Normal file
78
admin/deploy/values.yaml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Global / common settings
|
||||||
|
# =============================================================================
|
||||||
|
namespace: gitdataai
|
||||||
|
releaseName: admin
|
||||||
|
|
||||||
|
image:
|
||||||
|
registry: harbor.gitdata.me/gta_team
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
pullSecrets: [ ]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Cert-Manager Configuration (集群已安装 cert-manager)
|
||||||
|
# =============================================================================
|
||||||
|
certManager:
|
||||||
|
enabled: true
|
||||||
|
clusterIssuerName: cloudflare-acme-cluster-issuer
|
||||||
|
|
||||||
|
externalSecrets:
|
||||||
|
storeName: vault-backend
|
||||||
|
storeKind: SecretStore
|
||||||
|
|
||||||
|
admin:
|
||||||
|
enabled: true
|
||||||
|
replicaCount: 2
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: admin
|
||||||
|
tag: latest
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 3000
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
hosts: [ ]
|
||||||
|
annotations: { }
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
|
||||||
|
livenessProbe:
|
||||||
|
path: /api/health
|
||||||
|
port: 3000
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
|
||||||
|
readinessProbe:
|
||||||
|
path: /api/health
|
||||||
|
port: 3000
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
|
||||||
|
config:
|
||||||
|
appUrl: ""
|
||||||
|
appDomain: ""
|
||||||
|
nodeEnv: production
|
||||||
|
|
||||||
|
secretKeys:
|
||||||
|
databaseUrl: APP_DATABASE_URL
|
||||||
|
redisUrl: APP_REDIS_URL
|
||||||
|
nextAuthSecret: APP_NEXTAUTH_SECRET
|
||||||
|
|
||||||
|
env: [ ]
|
||||||
|
|
||||||
|
nodeSelector: { }
|
||||||
|
tolerations: [ ]
|
||||||
|
affinity: { }
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
enabled: false
|
||||||
|
databaseUrl: ""
|
||||||
|
redisUrl: ""
|
||||||
|
nextAuthSecret: ""
|
||||||
|
extra: { }
|
||||||
81
admin/scripts/build.js
Normal file
81
admin/scripts/build.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Build Docker image for Admin
|
||||||
|
*
|
||||||
|
* Workflow:
|
||||||
|
* 1. npm ci --legacy-peer-deps (install dependencies)
|
||||||
|
* 2. npm run build (Next.js build, outputs to .next/)
|
||||||
|
* 3. docker build (multi-stage, minimal runtime image)
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/build.js # Build image
|
||||||
|
* node scripts/build.js --no-cache # Build without Docker layer cache
|
||||||
|
*
|
||||||
|
* Environment:
|
||||||
|
* REGISTRY - Docker registry (default: harbor.gitdata.me/gta_team)
|
||||||
|
* TAG - Image tag (default: git short SHA)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const REGISTRY = process.env.REGISTRY || 'harbor.gitdata.me/gta_team';
|
||||||
|
const GIT_SHA_SHORT = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
|
||||||
|
const TAG = process.env.TAG || GIT_SHA_SHORT;
|
||||||
|
const SERVICE = 'admin';
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const noCache = args.includes('--no-cache');
|
||||||
|
const rootDir = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
console.log(`\n=== Build Configuration ===`);
|
||||||
|
console.log(`Registry: ${REGISTRY}`);
|
||||||
|
console.log(`Tag: ${TAG}`);
|
||||||
|
console.log(`Service: ${SERVICE}`);
|
||||||
|
console.log(`No Cache: ${noCache}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Step 1: npm ci
|
||||||
|
console.log(`==> Step 1: Installing dependencies (npm ci)`);
|
||||||
|
try {
|
||||||
|
execSync(`npm ci --legacy-peer-deps`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
cwd: rootDir,
|
||||||
|
});
|
||||||
|
console.log(` [OK] Dependencies installed`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` [FAIL] npm ci failed`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Next.js build
|
||||||
|
console.log(`\n==> Step 2: Building Next.js (npm run build)`);
|
||||||
|
try {
|
||||||
|
execSync(`npm run build`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
cwd: rootDir,
|
||||||
|
env: { ...process.env, NEXT_TELEMETRY_DISABLED: '1' },
|
||||||
|
});
|
||||||
|
console.log(` [OK] Next.js build complete`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` [FAIL] Next.js build failed`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Docker build
|
||||||
|
console.log(`\n==> Step 3: Building Docker image`);
|
||||||
|
const dockerfile = path.join(rootDir, 'Dockerfile');
|
||||||
|
const image = `${REGISTRY}/${SERVICE}:${TAG}`;
|
||||||
|
const buildCmd = `docker build ${noCache ? '--no-cache' : ''} -f "${dockerfile}" -t "${image}" .`;
|
||||||
|
|
||||||
|
console.log(` Building ${image}`);
|
||||||
|
try {
|
||||||
|
execSync(buildCmd, { stdio: 'inherit', cwd: rootDir });
|
||||||
|
console.log(` [OK] ${image}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` [FAIL] Docker build failed`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n=== Build Complete ===`);
|
||||||
|
console.log(` ${image}`);
|
||||||
|
console.log('');
|
||||||
110
admin/scripts/deploy.js
Normal file
110
admin/scripts/deploy.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Deploy Admin via Helm
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/deploy.js # Deploy with defaults
|
||||||
|
* node scripts/deploy.js --dry-run # Dry run only
|
||||||
|
*
|
||||||
|
* Environment:
|
||||||
|
* REGISTRY - Docker registry (default: harbor.gitdata.me/gta_team)
|
||||||
|
* TAG - Image tag (default: git short SHA)
|
||||||
|
* NAMESPACE - Kubernetes namespace (default: gitdataai)
|
||||||
|
* RELEASE - Helm release name (default: admin)
|
||||||
|
* KUBECONFIG - Path to kubeconfig (default: ~/.kube/config)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const REGISTRY = process.env.REGISTRY || 'harbor.gitdata.me/gta_team';
|
||||||
|
const GIT_SHA_SHORT = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
|
||||||
|
const TAG = process.env.TAG || process.env.GITHUB_SHA?.substring(0, 8) || GIT_SHA_SHORT;
|
||||||
|
const NAMESPACE = process.env.NAMESPACE || 'gitdataai';
|
||||||
|
const RELEASE = process.env.RELEASE || 'admin';
|
||||||
|
const CHART_PATH = path.join(__dirname, '..', 'deploy');
|
||||||
|
const KUBECONFIG = process.env.KUBECONFIG || path.join(
|
||||||
|
process.env.HOME || process.env.USERPROFILE,
|
||||||
|
'.kube',
|
||||||
|
'config'
|
||||||
|
);
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const isDryRun = args.includes('--dry-run');
|
||||||
|
|
||||||
|
const SERVICE = 'admin';
|
||||||
|
|
||||||
|
// Validate kubeconfig
|
||||||
|
if (!fs.existsSync(KUBECONFIG)) {
|
||||||
|
console.error(`Error: kubeconfig not found at ${KUBECONFIG}`);
|
||||||
|
console.error('Set KUBECONFIG environment variable or ensure ~/.kube/config exists');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n=== Deploy Configuration ===`);
|
||||||
|
console.log(`Registry: ${REGISTRY}`);
|
||||||
|
console.log(`Tag: ${TAG}`);
|
||||||
|
console.log(`Namespace: ${NAMESPACE}`);
|
||||||
|
console.log(`Release: ${RELEASE}`);
|
||||||
|
console.log(`Service: ${SERVICE}`);
|
||||||
|
console.log(`Dry Run: ${isDryRun}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Build helm values override
|
||||||
|
const valuesFile = path.join(CHART_PATH, 'values.yaml');
|
||||||
|
const userValuesFile = path.join(CHART_PATH, 'values.user.yaml');
|
||||||
|
|
||||||
|
const setValues = [
|
||||||
|
`image.registry=${REGISTRY}`,
|
||||||
|
`admin.image.tag=${TAG}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const helmArgs = [
|
||||||
|
'upgrade', '--install', RELEASE, CHART_PATH,
|
||||||
|
'--namespace', NAMESPACE,
|
||||||
|
'--create-namespace',
|
||||||
|
'-f', valuesFile,
|
||||||
|
...(fs.existsSync(userValuesFile) ? ['-f', userValuesFile] : []),
|
||||||
|
...setValues.flatMap(v => ['--set', v]),
|
||||||
|
'--wait',
|
||||||
|
'--timeout', '5m',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isDryRun) {
|
||||||
|
helmArgs.push('--dry-run');
|
||||||
|
console.log('==> Dry run mode - no changes will be made\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helm upgrade
|
||||||
|
console.log(`==> Running helm upgrade`);
|
||||||
|
console.log(` Command: helm ${helmArgs.join(' ')}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`helm ${helmArgs.join(' ')}`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: { ...process.env, KUBECONFIG },
|
||||||
|
});
|
||||||
|
console.log(`\n[OK] Deployment ${isDryRun ? '(dry-run) ' : ''}complete`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n[FAIL] Deployment failed');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rollout status
|
||||||
|
if (!isDryRun) {
|
||||||
|
console.log('\n==> Checking rollout status');
|
||||||
|
const deploymentName = `${RELEASE}-${SERVICE}`;
|
||||||
|
console.log(` Checking ${deploymentName}...`);
|
||||||
|
try {
|
||||||
|
execSync(
|
||||||
|
`kubectl rollout status deployment/${deploymentName} -n ${NAMESPACE} --timeout=120s`,
|
||||||
|
{ stdio: 'pipe', env: { ...process.env, KUBECONFIG } }
|
||||||
|
);
|
||||||
|
console.log(` [OK] ${deploymentName}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` [WARN] ${deploymentName} rollout timeout or failed`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== Deploy Complete ===\n');
|
||||||
73
admin/scripts/push.js
Normal file
73
admin/scripts/push.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Push Admin Docker image to registry
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/push.js # Push image
|
||||||
|
* node scripts/push.js --dry-run # Show what would be pushed
|
||||||
|
*
|
||||||
|
* Environment:
|
||||||
|
* REGISTRY - Docker registry (default: harbor.gitdata.me/gta_team)
|
||||||
|
* TAG - Image tag (default: git short SHA)
|
||||||
|
* DOCKER_USER - Registry username
|
||||||
|
* DOCKER_PASS - Registry password
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const REGISTRY = process.env.REGISTRY || 'harbor.gitdata.me/gta_team';
|
||||||
|
const GIT_SHA_SHORT = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
|
||||||
|
const TAG = process.env.TAG || GIT_SHA_SHORT;
|
||||||
|
const SERVICE = 'admin';
|
||||||
|
const DOCKER_USER = process.env.DOCKER_USER || process.env.HARBOR_USERNAME;
|
||||||
|
const DOCKER_PASS = process.env.DOCKER_PASS || process.env.HARBOR_PASSWORD;
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const isDryRun = args.includes('--dry-run');
|
||||||
|
|
||||||
|
if (!DOCKER_USER || !DOCKER_PASS) {
|
||||||
|
console.error('Error: DOCKER_USER and DOCKER_PASS environment variables are required');
|
||||||
|
console.error('Set HARBOR_USERNAME and HARBOR_PASSWORD as alternative');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = `${REGISTRY}/${SERVICE}:${TAG}`;
|
||||||
|
|
||||||
|
console.log(`\n=== Push Configuration ===`);
|
||||||
|
console.log(`Registry: ${REGISTRY}`);
|
||||||
|
console.log(`Tag: ${TAG}`);
|
||||||
|
console.log(`Image: ${image}`);
|
||||||
|
console.log(`Dry Run: ${isDryRun}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
if (isDryRun) {
|
||||||
|
console.log('[DRY RUN] Would push:', image);
|
||||||
|
console.log('');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login
|
||||||
|
console.log(`==> Logging in to ${REGISTRY}`);
|
||||||
|
try {
|
||||||
|
execSync(`docker login ${REGISTRY} -u "${DOCKER_USER}" -p "${DOCKER_PASS}"`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login failed');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push
|
||||||
|
console.log(`\n==> Pushing ${image}`);
|
||||||
|
try {
|
||||||
|
execSync(`docker push "${image}"`, { stdio: 'inherit' });
|
||||||
|
console.log(` [OK] ${image}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` [FAIL] Push failed`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n=== Push Complete ===`);
|
||||||
|
console.log(` ${image}`);
|
||||||
|
console.log('');
|
||||||
Loading…
Reference in New Issue
Block a user