feat(aiproxy): add aiproxy chart build (#6073)

* feat(aiproxy): add chart and Dockerfile for aiproxy frontend

Signed-off-by: cuisongliu <cuisongliu@qq.com>

* feat(aiproxy): add app configuration and update install script for suffix handling

Signed-off-by: cuisongliu <cuisongliu@qq.com>

* fix(aiproxy): remove space in URL construction for proper formatting

Signed-off-by: cuisongliu <cuisongliu@qq.com>

---------

Signed-off-by: cuisongliu <cuisongliu@qq.com>
This commit is contained in:
cuisongliu
2025-10-11 21:14:58 +08:00
committed by GitHub
parent 73dc3a22fb
commit 2b33ea9b4e
14 changed files with 362 additions and 2 deletions
+8 -2
View File
@@ -298,8 +298,14 @@ jobs:
ALIYUN_CLUSTER_IMAGE: ${{ steps.prepare.outputs.aliyun_cluster_image }}
ALIYUN_LATEST_CLUSTER_IMAGE: ${{ steps.prepare.outputs.aliyun_latest_cluster_image }}
run: |
sudo sed -i "s;${OLD_DOCKER_IMAGE};${NEW_DOCKER_IMAGE};" manifests/*
if [[ -d manifests ]]; then
echo "Found manifests"
sudo sed -i "s;${OLD_DOCKER_IMAGE};${NEW_DOCKER_IMAGE};" manifests/*
fi
if [[ -f charts/${{ env.MODULE_NAME }}/values.yaml ]]; then
echo "Found charts charts/${{ env.MODULE_NAME }}/values.yaml"
sed -i "s;${OLD_DOCKER_IMAGE};${NEW_DOCKER_IMAGE};" charts/${{ env.MODULE_NAME }}/values.yaml
fi
# Build for GHCR
sudo sealos build -t "${CLUSTER_IMAGE}-amd64" --platform linux/amd64 -f Kubefile
sudo sealos build -t "${LATEST_CLUSTER_IMAGE}-amd64" --platform linux/amd64 -f Kubefile
+1
View File
@@ -44,6 +44,7 @@ jobs:
matrix:
module:
[
providers/aiproxy,
providers/license,
providers/cronjob,
providers/template,
+58
View File
@@ -0,0 +1,58 @@
# Install dependencies only when needed
FROM node:current-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat && npm install -g pnpm
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json pnpm-lock.yaml* ./
RUN \
[ -f pnpm-lock.yaml ] && pnpm install || \
(echo "Lockfile not found." && exit 1)
# Rebuild the source code only when needed
FROM node:current-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
ENV NEXT_PUBLIC_MOCK_USER ''
RUN npm install -g pnpm && pnpm run build
# Production image, copy all the files and run next
FROM node:current-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN sed -i 's/https/http/' /etc/apk/repositories
RUN apk add curl \
&& apk add ca-certificates \
&& update-ca-certificates
# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
# COPY --from=builder /app/.env* .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
@@ -0,0 +1,3 @@
FROM scratch
COPY . .
CMD ["bash install.sh"]
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
@@ -0,0 +1,24 @@
apiVersion: v2
name: aiproxy-web
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"
@@ -0,0 +1,58 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "aiproxy.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "aiproxy.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "aiproxy.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "aiproxy.labels" -}}
helm.sh/chart: {{ include "aiproxy.chart" . }}
{{ include "aiproxy.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "aiproxy.selectorLabels" -}}
app.kubernetes.io/name: {{ include "aiproxy.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "aiproxy.serviceAccountName" -}}
{{- default (include "aiproxy.fullname" .) .Values.serviceAccount.name }}
{{- end }}
@@ -0,0 +1,18 @@
apiVersion: app.sealos.io/v1
kind: App
metadata:
name: aiproxy
namespace: app-system
spec:
data:
desc: Sealos AI Proxy
url: https://{{ include "aiproxy.fullname" . }}.{{.Values.cloudDomain}}{{.Values.suffix}}
displayType: more
i18n:
zh:
name: AI Proxy
zh-Hans:
name: AI Proxy
icon: https://objectstorageapi.hzh.sealos.run/cyhipdvv-logos/aiproxy.svg
name: AI Proxy
type: iframe
@@ -0,0 +1,8 @@
apiVersion: v1
data: {{ toYaml .Values.aiproxy | nindent 2 }}
kind: ConfigMap
metadata:
namespace: {{.Release.Namespace}}
name: {{ include "aiproxy.fullname" . }}
labels:
{{- include "aiproxy.labels" . | nindent 4 }}
@@ -0,0 +1,59 @@
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: {{ .Release.Namespace }}
name: {{ include "aiproxy.fullname" . }}
labels:
{{- include "aiproxy.labels" . | nindent 4 }}
spec:
replicas: {{.Values.replicas}}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
{{ include "aiproxy.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
{{- with .Values.podAnnotations }}
{{ toYaml . | nindent 8 }}
{{- end }}
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
{{ include "aiproxy.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{ toYaml . | nindent 8 }}
{{- end }}
spec:
terminationGracePeriodSeconds: 600
containers:
- image: {{.Values.image}}
imagePullPolicy: IfNotPresent
name: aiproxy
resources: {{ toYaml .Values.resources | nindent 12 }}
env:
- name: AI_PROXY_BACKEND
value: https://aiproxy.{{.Values.cloudDomain}}
envFrom:
- configMapRef:
name: {{ include "aiproxy.fullname" . }}
ports:
- name: http
containerPort: 3000
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
namespace: {{ .Release.Namespace }}
name: {{ include "aiproxy.fullname" . }}
labels:
{{- include "aiproxy.labels" . | nindent 4 }}
spec:
ports:
- port: 3000
targetPort: 3000
selector: {{ include "aiproxy.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,34 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: {{.Release.Namespace}}
name: {{ include "aiproxy.fullname" . }}
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/proxy-body-size: 32m
nginx.ingress.kubernetes.io/ssl-redirect: 'false'
nginx.ingress.kubernetes.io/backend-protocol: HTTP
nginx.ingress.kubernetes.io/client-body-buffer-size: 64k
nginx.ingress.kubernetes.io/proxy-buffer-size: 64k
nginx.ingress.kubernetes.io/proxy-send-timeout: '300'
nginx.ingress.kubernetes.io/proxy-read-timeout: '300'
nginx.ingress.kubernetes.io/server-snippet: |
client_header_buffer_size 64k;
large_client_header_buffers 4 128k;
spec:
rules:
- host: {{ include "aiproxy.fullname" . }}.{{.Values.cloudDomain}}
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: {{ include "aiproxy.fullname" . }}
port:
number: 3000
tls:
- hosts:
- {{ include "aiproxy.fullname" . }}.{{.Values.cloudDomain}}
secretName: wildcard-cert
@@ -0,0 +1,28 @@
fullnameOverride: aiproxy-web
replicas: 2
resources:
requests:
cpu: 50m
memory: 50Mi
limits:
cpu: 500m
memory: 512Mi
image: ghcr.io/labring/sealos-aiproxy-frontend:latest
cloudDomain: 127.0.0.1.nip.io
cloudPort: 443
suffix:
aiproxy:
AI_PROXY_BACKEND_KEY: ""
APP_TOKEN_JWT_KEY: ""
AI_PROXY_BACKEND_INTERNAL: "http://aiproxy.apiproxy-system.svc:3000"
ADMIN_NAMESPACES: "ns-admin"
ACCOUNT_SERVER_TOKEN_JWT_KEY: ""
CURRENCY_SYMBOL: ""
ACCOUNT_SERVER: "http://account-service.account-system.svc:2333"
IS_INVITATION_ACTIVE: "false"
DOC_URL: "https://sealos.run/docs/guides/ai-proxy"
@@ -0,0 +1,40 @@
#!/bin/bash
timestamp() {
date +"%Y-%m-%d %T"
}
print() {
flag=$(timestamp)
echo -e "\033[1;32m\033[1m INFO [$flag] >> $* \033[0m"
}
warn() {
flag=$(timestamp)
echo -e "\033[33m WARN [$flag] >> $* \033[0m"
}
info() {
flag=$(timestamp)
echo -e "\033[36m INFO [$flag] >> $* \033[0m"
}
#===========================================================================
HELM_OPTS=${HELM_OPTS:-""}
NODE_COUNT=$(kubectl get nodes --no-headers | wc -l)
REPLICA_OPTIONS=""
if [ "$NODE_COUNT" -eq 1 ]; then
REPLICA_OPTIONS="--set replicas=1 "
HELM_OPTS="${HELM_OPTS} ${REPLICA_OPTIONS}"
fi
varJwtInternal=$(kubectl get configmap sealos-config -n sealos-system -o jsonpath='{.data.jwtInternal}')
adminKey=$(kubectl get configmap aiproxy-env -n aiproxy-system -o jsonpath='{.data.ADMIN_KEY}' )
SEALOS_CLOUD_DOMAIN=$(kubectl get configmap sealos-config -n sealos-system -o jsonpath='{.data.cloudDomain}')
SEALOS_CLOUD_PORT=$(kubectl get configmap sealos-config -n sealos-system -o jsonpath='{.data.cloudPort}')
CURRENCY_SYMBOL=$(kubectl get deployment -n dbprovider-frontend dbprovider-frontend -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="CURRENCY_SYMBOL")].value}')
if [[ "${CURRENCY_SYMBOL}" == "usd" ]]; then
SUFFIX="/en/home"
fi
helm upgrade -i aiproxy-web -n aiproxy-system charts/aiproxy ${HELM_OPTS} \
--set aiproxy.APP_TOKEN_JWT_KEY=${varJwtInternal} --set aiproxy.AI_PROXY_BACKEND_KEY=${adminKey} \
--set suffix=${SUFFIX} \
--set cloudDomain=${SEALOS_CLOUD_DOMAIN} --set cloudPort=${SEALOS_CLOUD_PORT} --set aiproxy.CURRENCY_SYMBOL=${CURRENCY_SYMBOL} --create-namespace