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