From 1a2410f54157bb1f9f489893a6438194b094ff27 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Sat, 7 Feb 2026 04:52:25 +0800 Subject: [PATCH] feat: merge service (#14384) --- .github/actions/deploy/deploy.mjs | 84 +++---- .github/deployment/front/Dockerfile | 13 -- .github/deployment/front/affine.nginx.conf | 42 ---- .github/deployment/front/nginx.conf | 15 -- .../affine/charts/{sync => front}/Chart.yaml | 4 +- .../{renderer => front}/templates/NOTES.txt | 16 +- .../{sync => front}/templates/_helpers.tpl | 20 +- .../charts/front/templates/deployment.yaml | 120 ++++++++++ .../front/templates/service-renderer.yaml | 19 ++ .../charts/front/templates/service-sync.yaml | 19 ++ .../charts/front/templates/service-web.yaml | 19 ++ .../templates/serviceaccount.yaml | 4 +- .../templates/tests/test-connection.yaml | 6 +- .github/helm/affine/charts/front/values.yaml | 60 +++++ .../helm/affine/charts/renderer/Chart.yaml | 11 - .../charts/renderer/templates/_helpers.tpl | 63 ------ .../charts/renderer/templates/deployment.yaml | 118 ---------- .../charts/renderer/templates/service.yaml | 19 -- .../renderer/templates/serviceaccount.yaml | 12 - .../templates/tests/test-connection.yaml | 15 -- .../helm/affine/charts/renderer/values.yaml | 38 ---- .github/helm/affine/charts/sync/.helmignore | 23 -- .../affine/charts/sync/templates/NOTES.txt | 16 -- .../charts/sync/templates/deployment.yaml | 112 ---------- .../affine/charts/sync/templates/service.yaml | 19 -- .../charts/sync/templates/serviceaccount.yaml | 12 - .../sync/templates/tests/test-connection.yaml | 15 -- .github/helm/affine/charts/sync/values.yaml | 38 ---- .github/helm/affine/charts/web/.helmignore | 23 -- .github/helm/affine/charts/web/Chart.yaml | 6 - .../affine/charts/web/templates/NOTES.txt | 16 -- .../affine/charts/web/templates/_helpers.tpl | 63 ------ .../charts/web/templates/deployment.yaml | 60 ----- .../affine/charts/web/templates/service.yaml | 15 -- .github/helm/affine/charts/web/values.yaml | 37 ---- .github/helm/affine/templates/ingress.yaml | 12 +- .github/helm/affine/values.yaml | 36 +-- .github/workflows/build-images.yml | 13 +- .../src/__tests__/e2e/apps/flavors.spec.ts | 9 + .../backend/server/src/__tests__/env.spec.ts | 29 ++- packages/backend/server/src/app.module.ts | 17 +- .../backend/server/src/core/selfhost/setup.ts | 5 + .../static-files/__tests__/static.spec.ts | 193 ++++++++++++++++ .../server/src/core/static-files/index.ts | 8 + .../server/src/core/static-files/static.ts | 119 ++++++++++ .../core/utils/__tests__/user-agent.spec.ts | 65 ++++++ .../server/src/core/utils/user-agent.ts | 207 ++++++++++++++++++ packages/backend/server/src/env.ts | 2 + scripts/set-version.sh | 3 +- 49 files changed, 966 insertions(+), 924 deletions(-) delete mode 100644 .github/deployment/front/Dockerfile delete mode 100644 .github/deployment/front/affine.nginx.conf delete mode 100644 .github/deployment/front/nginx.conf rename .github/helm/affine/charts/{sync => front}/Chart.yaml (83%) rename .github/helm/affine/charts/{renderer => front}/templates/NOTES.txt (55%) rename .github/helm/affine/charts/{sync => front}/templates/_helpers.tpl (77%) create mode 100644 .github/helm/affine/charts/front/templates/deployment.yaml create mode 100644 .github/helm/affine/charts/front/templates/service-renderer.yaml create mode 100644 .github/helm/affine/charts/front/templates/service-sync.yaml create mode 100644 .github/helm/affine/charts/front/templates/service-web.yaml rename .github/helm/affine/charts/{web => front}/templates/serviceaccount.yaml (68%) rename .github/helm/affine/charts/{web => front}/templates/tests/test-connection.yaml (50%) create mode 100644 .github/helm/affine/charts/front/values.yaml delete mode 100644 .github/helm/affine/charts/renderer/Chart.yaml delete mode 100644 .github/helm/affine/charts/renderer/templates/_helpers.tpl delete mode 100644 .github/helm/affine/charts/renderer/templates/deployment.yaml delete mode 100644 .github/helm/affine/charts/renderer/templates/service.yaml delete mode 100644 .github/helm/affine/charts/renderer/templates/serviceaccount.yaml delete mode 100644 .github/helm/affine/charts/renderer/templates/tests/test-connection.yaml delete mode 100644 .github/helm/affine/charts/renderer/values.yaml delete mode 100644 .github/helm/affine/charts/sync/.helmignore delete mode 100644 .github/helm/affine/charts/sync/templates/NOTES.txt delete mode 100644 .github/helm/affine/charts/sync/templates/deployment.yaml delete mode 100644 .github/helm/affine/charts/sync/templates/service.yaml delete mode 100644 .github/helm/affine/charts/sync/templates/serviceaccount.yaml delete mode 100644 .github/helm/affine/charts/sync/templates/tests/test-connection.yaml delete mode 100644 .github/helm/affine/charts/sync/values.yaml delete mode 100644 .github/helm/affine/charts/web/.helmignore delete mode 100644 .github/helm/affine/charts/web/Chart.yaml delete mode 100644 .github/helm/affine/charts/web/templates/NOTES.txt delete mode 100644 .github/helm/affine/charts/web/templates/_helpers.tpl delete mode 100644 .github/helm/affine/charts/web/templates/deployment.yaml delete mode 100644 .github/helm/affine/charts/web/templates/service.yaml delete mode 100644 .github/helm/affine/charts/web/values.yaml create mode 100644 packages/backend/server/src/core/static-files/__tests__/static.spec.ts create mode 100644 packages/backend/server/src/core/static-files/index.ts create mode 100644 packages/backend/server/src/core/static-files/static.ts create mode 100644 packages/backend/server/src/core/utils/__tests__/user-agent.spec.ts create mode 100644 packages/backend/server/src/core/utils/user-agent.ts diff --git a/.github/actions/deploy/deploy.mjs b/.github/actions/deploy/deploy.mjs index 4ba6249757..9ec2e4e502 100644 --- a/.github/actions/deploy/deploy.mjs +++ b/.github/actions/deploy/deploy.mjs @@ -29,43 +29,26 @@ const isInternal = buildType === 'internal'; const replicaConfig = { stable: { - web: 2, + front: Number(process.env.PRODUCTION_FRONT_REPLICA) || 2, graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2, - sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 2, - renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 2, doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 2, }, beta: { - web: 1, + front: Number(process.env.BETA_FRONT_REPLICA) || 1, graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1, - sync: Number(process.env.BETA_SYNC_REPLICA) || 1, - renderer: Number(process.env.BETA_RENDERER_REPLICA) || 1, doc: Number(process.env.BETA_DOC_REPLICA) || 1, }, - canary: { - web: 1, - graphql: 1, - sync: 1, - renderer: 1, - doc: 1, - }, + canary: { front: 1, graphql: 1, doc: 1 }, }; const cpuConfig = { - beta: { - web: '300m', - graphql: '1', - sync: '1', - doc: '1', - renderer: '300m', - }, - canary: { - web: '300m', - graphql: '1', - sync: '1', - doc: '1', - renderer: '300m', - }, + beta: { front: '2', graphql: '1', doc: '1' }, + canary: { front: '500m', graphql: '1', doc: '500m' }, +}; + +const memoryConfig = { + beta: { front: '1Gi', graphql: '1Gi', doc: '1Gi' }, + canary: { front: '512Mi', graphql: '512Mi', doc: '512Mi' }, }; const createHelmCommand = ({ isDryRun }) => { @@ -90,16 +73,16 @@ const createHelmCommand = ({ isDryRun }) => { `--set-string global.indexer.apiKey="${AFFINE_INDEXER_SEARCH_API_KEY}"`, ]; const serviceAnnotations = [ - `--set-json web.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`, + `--set-json front.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`, `--set-json graphql.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`, - `--set-json sync.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`, `--set-json doc.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`, ].concat( isProduction || isBeta || isInternal ? [ - `--set-json web.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`, + `--set-json front.services.web.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`, + `--set-json front.services.sync.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`, + `--set-json front.services.renderer.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`, `--set-json graphql.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`, - `--set-json sync.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`, `--set-json cloud-sql-proxy.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }"`, `--set-json cloud-sql-proxy.nodeSelector="{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }"`, ] @@ -107,14 +90,22 @@ const createHelmCommand = ({ isDryRun }) => { ); const cpu = cpuConfig[buildType]; - const resources = cpu - ? [ - `--set web.resources.requests.cpu="${cpu.web}"`, - `--set graphql.resources.requests.cpu="${cpu.graphql}"`, - `--set sync.resources.requests.cpu="${cpu.sync}"`, - `--set doc.resources.requests.cpu="${cpu.doc}"`, - ] - : []; + const memory = memoryConfig[buildType]; + let resources = []; + if (cpu) { + resources = resources.concat([ + `--set front.resources.requests.cpu="${cpu.front}"`, + `--set graphql.resources.requests.cpu="${cpu.graphql}"`, + `--set doc.resources.requests.cpu="${cpu.doc}"`, + ]); + } + if (memory) { + resources = resources.concat([ + `--set front.resources.requests.memory="${memory.front}"`, + `--set graphql.resources.requests.memory="${memory.graphql}"`, + `--set doc.resources.requests.memory="${memory.doc}"`, + ]); + } const replica = replicaConfig[buildType] || replicaConfig.canary; @@ -130,6 +121,7 @@ const createHelmCommand = ({ isDryRun }) => { .split(',') .map(host => host.trim()) .filter(host => host); + const primaryHost = hosts[0] || '0.0.0.0'; const deployCommand = [ `helm upgrade --install affine .github/helm/affine`, `--namespace ${namespace}`, @@ -144,18 +136,14 @@ const createHelmCommand = ({ isDryRun }) => { `--set-string global.version="${APP_VERSION}"`, ...redisAndPostgres, ...indexerOptions, - `--set web.replicaCount=${replica.web}`, - `--set-string web.image.tag="${imageTag}"`, + `--set front.replicaCount=${replica.front}`, + `--set-string front.image.tag="${imageTag}"`, + `--set-string front.app.host="${primaryHost}"`, `--set graphql.replicaCount=${replica.graphql}`, `--set-string graphql.image.tag="${imageTag}"`, - `--set graphql.app.host=${hosts[0]}`, - `--set sync.replicaCount=${replica.sync}`, - `--set-string sync.image.tag="${imageTag}"`, - `--set-string renderer.image.tag="${imageTag}"`, - `--set renderer.app.host=${hosts[0]}`, - `--set renderer.replicaCount=${replica.renderer}`, + `--set-string graphql.app.host="${primaryHost}"`, `--set-string doc.image.tag="${imageTag}"`, - `--set doc.app.host=${hosts[0]}`, + `--set-string doc.app.host="${primaryHost}"`, `--set doc.replicaCount=${replica.doc}`, ...serviceAnnotations, ...resources, diff --git a/.github/deployment/front/Dockerfile b/.github/deployment/front/Dockerfile deleted file mode 100644 index 82444c1710..0000000000 --- a/.github/deployment/front/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM openresty/openresty:1.27.1.1-0-buster -WORKDIR /app -COPY ./packages/frontend/apps/web/dist ./dist -COPY ./packages/frontend/admin/dist ./admin -COPY ./packages/frontend/apps/mobile/dist ./mobile -COPY ./.github/deployment/front/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf -COPY ./.github/deployment/front/affine.nginx.conf /etc/nginx/conf.d/affine.nginx.conf - -RUN mkdir -p /var/log/nginx && \ - rm /etc/nginx/conf.d/default.conf - -EXPOSE 8080 -CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"] diff --git a/.github/deployment/front/affine.nginx.conf b/.github/deployment/front/affine.nginx.conf deleted file mode 100644 index 8e2a2d352c..0000000000 --- a/.github/deployment/front/affine.nginx.conf +++ /dev/null @@ -1,42 +0,0 @@ -server { - listen 8080; - location /admin { - root /app/; - index index.html; - try_files $uri/index.html $uri/ $uri /admin/index.html; - } - - set $app_root_path /app/dist/; - set $mobile_root /app/dist/; - set_by_lua $affine_env 'return os.getenv("AFFINE_ENV")'; - - if ($affine_env = "dev") { - set $mobile_root /app/mobile/; - } - - # https://gist.github.com/mariusom/6683dc52b1cad1a1f372e908bdb209d0 - if ($http_user_agent ~* "(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino") { - set $app_root_path $mobile_root; - } - - if ($http_user_agent ~* "^(1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-)") { - set $app_root_path $mobile_root; - } - - location ~ ^/(_plugin|assets|imgs|js|plugins|static)/ { - root $app_root_path; - try_files $uri $uri/ =404; - } - - location / { - root $app_root_path; - index index.html; - try_files $uri $uri/ /index.html; - add_header Cache-Control "private, no-cache, no-store, max-age=0, must-revalidate"; - } - - error_page 404 /404.html; - location = /404.html { - internal; - } -} diff --git a/.github/deployment/front/nginx.conf b/.github/deployment/front/nginx.conf deleted file mode 100644 index e982808a1b..0000000000 --- a/.github/deployment/front/nginx.conf +++ /dev/null @@ -1,15 +0,0 @@ -worker_processes 4; -error_log /var/log/nginx/error.log warn; -pcre_jit on; -env AFFINE_ENV; -events { - worker_connections 1024; -} -http { - include mime.types; - log_format main '$remote_addr [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /var/log/nginx/access.log main; - include /etc/nginx/conf.d/*.conf; -} diff --git a/.github/helm/affine/charts/sync/Chart.yaml b/.github/helm/affine/charts/front/Chart.yaml similarity index 83% rename from .github/helm/affine/charts/sync/Chart.yaml rename to .github/helm/affine/charts/front/Chart.yaml index d217833754..f7f9668a68 100644 --- a/.github/helm/affine/charts/sync/Chart.yaml +++ b/.github/helm/affine/charts/front/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: sync -description: AFFiNE Sync Server +name: front +description: AFFiNE front server type: application version: 0.0.0 appVersion: "0.26.0" diff --git a/.github/helm/affine/charts/renderer/templates/NOTES.txt b/.github/helm/affine/charts/front/templates/NOTES.txt similarity index 55% rename from .github/helm/affine/charts/renderer/templates/NOTES.txt rename to .github/helm/affine/charts/front/templates/NOTES.txt index 805c45e12a..796f60e961 100644 --- a/.github/helm/affine/charts/renderer/templates/NOTES.txt +++ b/.github/helm/affine/charts/front/templates/NOTES.txt @@ -1,15 +1,15 @@ 1. Get the application URL by running these commands: -{{- if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "renderer.fullname" . }}) +{{- if contains "NodePort" .Values.services.sync.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ .Values.services.sync.name }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} +{{- else if contains "LoadBalancer" .Values.services.sync.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "renderer.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "renderer.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "renderer.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ .Values.services.sync.name }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ .Values.services.sync.name }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.services.sync.port }} +{{- else if contains "ClusterIP" .Values.services.sync.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "front.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT diff --git a/.github/helm/affine/charts/sync/templates/_helpers.tpl b/.github/helm/affine/charts/front/templates/_helpers.tpl similarity index 77% rename from .github/helm/affine/charts/sync/templates/_helpers.tpl rename to .github/helm/affine/charts/front/templates/_helpers.tpl index 1c0337ff72..dcfa4a6cca 100644 --- a/.github/helm/affine/charts/sync/templates/_helpers.tpl +++ b/.github/helm/affine/charts/front/templates/_helpers.tpl @@ -1,7 +1,7 @@ {{/* Expand the name of the chart. */}} -{{- define "sync.name" -}} +{{- define "front.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} @@ -10,7 +10,7 @@ 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 "sync.fullname" -}} +{{- define "front.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} @@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "sync.chart" -}} +{{- define "front.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "sync.labels" -}} -helm.sh/chart: {{ include "sync.chart" . }} -{{ include "sync.selectorLabels" . }} +{{- define "front.labels" -}} +helm.sh/chart: {{ include "front.chart" . }} +{{ include "front.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -46,17 +46,17 @@ monitoring: enabled {{/* Selector labels */}} -{{- define "sync.selectorLabels" -}} -app.kubernetes.io/name: {{ include "sync.name" . }} +{{- define "front.selectorLabels" -}} +app.kubernetes.io/name: {{ include "front.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} -{{- define "sync.serviceAccountName" -}} +{{- define "front.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} -{{- default (include "sync.fullname" .) .Values.serviceAccount.name }} +{{- default (include "front.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} diff --git a/.github/helm/affine/charts/front/templates/deployment.yaml b/.github/helm/affine/charts/front/templates/deployment.yaml new file mode 100644 index 0000000000..e17a168bce --- /dev/null +++ b/.github/helm/affine/charts/front/templates/deployment.yaml @@ -0,0 +1,120 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "front.fullname" . }} + labels: + {{- include "front.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "front.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "front.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "front.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: AFFINE_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: "{{ .Values.global.secret.secretName }}" + key: key + - name: NODE_ENV + value: "{{ .Values.env }}" + - name: NODE_OPTIONS + value: "{{ .Values.nodeOptions }}" + - name: NO_COLOR + value: "1" + - name: DEPLOYMENT_TYPE + value: "{{ .Values.global.deployment.type }}" + - name: DEPLOYMENT_PLATFORM + value: "{{ .Values.global.deployment.platform }}" + - name: SERVER_FLAVOR + value: "front" + - name: AFFINE_ENV + value: "{{ .Release.Namespace }}" + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: pg-postgresql + key: postgres-password + - name: DATABASE_URL + value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }} + - name: REDIS_SERVER_ENABLED + value: "true" + - name: REDIS_SERVER_HOST + value: "{{ .Values.global.redis.host }}" + - name: REDIS_SERVER_PORT + value: "{{ .Values.global.redis.port }}" + - name: REDIS_SERVER_USER + value: "{{ .Values.global.redis.username }}" + - name: REDIS_SERVER_PASSWORD + valueFrom: + secretKeyRef: + name: redis + key: redis-password + - name: REDIS_SERVER_DATABASE + value: "{{ .Values.global.redis.database }}" + - name: AFFINE_INDEXER_SEARCH_PROVIDER + value: "{{ .Values.global.indexer.provider }}" + - name: AFFINE_INDEXER_SEARCH_ENDPOINT + value: "{{ .Values.global.indexer.endpoint }}" + - name: AFFINE_INDEXER_SEARCH_API_KEY + valueFrom: + secretKeyRef: + name: indexer + key: indexer-apiKey + - name: AFFINE_SERVER_PORT + value: "{{ .Values.app.port }}" + - name: AFFINE_SERVER_SUB_PATH + value: "{{ .Values.app.path }}" + - name: AFFINE_SERVER_HOST + value: "{{ .Values.app.host }}" + - name: AFFINE_SERVER_HTTPS + value: "{{ .Values.app.https }}" + - name: DOC_SERVICE_ENDPOINT + value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}" + ports: + - name: http + containerPort: {{ .Values.app.port }} + protocol: TCP + livenessProbe: + httpGet: + path: /info + port: http + initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} + readinessProbe: + httpGet: + path: /info + port: http + initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/.github/helm/affine/charts/front/templates/service-renderer.yaml b/.github/helm/affine/charts/front/templates/service-renderer.yaml new file mode 100644 index 0000000000..070f2750f8 --- /dev/null +++ b/.github/helm/affine/charts/front/templates/service-renderer.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.services.renderer.name }} + labels: + {{- include "front.labels" . | nindent 4 }} + {{- with .Values.services.renderer.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.services.renderer.type }} + ports: + - port: {{ .Values.services.renderer.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "front.selectorLabels" . | nindent 4 }} diff --git a/.github/helm/affine/charts/front/templates/service-sync.yaml b/.github/helm/affine/charts/front/templates/service-sync.yaml new file mode 100644 index 0000000000..dfdbb0d6be --- /dev/null +++ b/.github/helm/affine/charts/front/templates/service-sync.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.services.sync.name }} + labels: + {{- include "front.labels" . | nindent 4 }} + {{- with .Values.services.sync.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.services.sync.type }} + ports: + - port: {{ .Values.services.sync.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "front.selectorLabels" . | nindent 4 }} diff --git a/.github/helm/affine/charts/front/templates/service-web.yaml b/.github/helm/affine/charts/front/templates/service-web.yaml new file mode 100644 index 0000000000..c98035d218 --- /dev/null +++ b/.github/helm/affine/charts/front/templates/service-web.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.services.web.name }} + labels: + {{- include "front.labels" . | nindent 4 }} + {{- with .Values.services.web.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.services.web.type }} + ports: + - port: {{ .Values.services.web.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "front.selectorLabels" . | nindent 4 }} diff --git a/.github/helm/affine/charts/web/templates/serviceaccount.yaml b/.github/helm/affine/charts/front/templates/serviceaccount.yaml similarity index 68% rename from .github/helm/affine/charts/web/templates/serviceaccount.yaml rename to .github/helm/affine/charts/front/templates/serviceaccount.yaml index f9f19b3454..322b290ec2 100644 --- a/.github/helm/affine/charts/web/templates/serviceaccount.yaml +++ b/.github/helm/affine/charts/front/templates/serviceaccount.yaml @@ -2,9 +2,9 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: {{ include "web.serviceAccountName" . }} + name: {{ include "front.serviceAccountName" . }} labels: - {{- include "web.labels" . | nindent 4 }} + {{- include "front.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} diff --git a/.github/helm/affine/charts/web/templates/tests/test-connection.yaml b/.github/helm/affine/charts/front/templates/tests/test-connection.yaml similarity index 50% rename from .github/helm/affine/charts/web/templates/tests/test-connection.yaml rename to .github/helm/affine/charts/front/templates/tests/test-connection.yaml index f36083416f..d917f44288 100644 --- a/.github/helm/affine/charts/web/templates/tests/test-connection.yaml +++ b/.github/helm/affine/charts/front/templates/tests/test-connection.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "web.fullname" . }}-test-connection" + name: "{{ include "front.fullname" . }}-test-connection" labels: - {{- include "web.labels" . | nindent 4 }} + {{- include "front.labels" . | nindent 4 }} annotations: "helm.sh/hook": test spec: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "web.fullname" . }}:{{ .Values.service.port }}'] + args: ['{{ .Values.services.sync.name }}:{{ .Values.services.sync.port }}'] restartPolicy: Never diff --git a/.github/helm/affine/charts/front/values.yaml b/.github/helm/affine/charts/front/values.yaml new file mode 100644 index 0000000000..1e005e5ea0 --- /dev/null +++ b/.github/helm/affine/charts/front/values.yaml @@ -0,0 +1,60 @@ +replicaCount: 1 +image: + repository: ghcr.io/toeverything/affine + pullPolicy: IfNotPresent + tag: '' + +imagePullSecrets: [] +nameOverride: '' +fullnameOverride: '' +# map to NODE_ENV environment variable +env: 'production' +nodeOptions: '--max-old-space-size=3072' +app: + # AFFINE_SERVER_PORT + port: 3010 + # AFFINE_SERVER_SUB_PATH + path: '' + # AFFINE_SERVER_HOST + host: '0.0.0.0' + https: true +serviceAccount: + create: true + annotations: {} + name: 'affine-front' + +podAnnotations: {} + +podSecurityContext: + fsGroup: 2000 + +resources: + requests: + cpu: '2' + memory: 4Gi + +probe: + initialDelaySeconds: 20 + +services: + sync: + name: affine-sync + type: ClusterIP + port: 3010 + annotations: + cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' + renderer: + name: affine-renderer + type: ClusterIP + port: 3000 + annotations: + cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' + web: + name: affine-web + type: ClusterIP + port: 8080 + annotations: {} + +nodeSelector: {} +tolerations: [] +affinity: {} diff --git a/.github/helm/affine/charts/renderer/Chart.yaml b/.github/helm/affine/charts/renderer/Chart.yaml deleted file mode 100644 index bd16e028de..0000000000 --- a/.github/helm/affine/charts/renderer/Chart.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v2 -name: renderer -description: AFFiNE renderer server -type: application -version: 0.0.0 -appVersion: "0.26.0" -dependencies: - - name: gcloud-sql-proxy - version: 0.0.0 - repository: "file://../gcloud-sql-proxy" - condition: .global.database.gcloud.enabled diff --git a/.github/helm/affine/charts/renderer/templates/_helpers.tpl b/.github/helm/affine/charts/renderer/templates/_helpers.tpl deleted file mode 100644 index 6a77a56d13..0000000000 --- a/.github/helm/affine/charts/renderer/templates/_helpers.tpl +++ /dev/null @@ -1,63 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "renderer.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 "renderer.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 "renderer.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "renderer.labels" -}} -helm.sh/chart: {{ include "renderer.chart" . }} -{{ include "renderer.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -monitoring: enabled -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "renderer.selectorLabels" -}} -app.kubernetes.io/name: {{ include "renderer.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "renderer.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "renderer.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/.github/helm/affine/charts/renderer/templates/deployment.yaml b/.github/helm/affine/charts/renderer/templates/deployment.yaml deleted file mode 100644 index 8ba3174f5d..0000000000 --- a/.github/helm/affine/charts/renderer/templates/deployment.yaml +++ /dev/null @@ -1,118 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "renderer.fullname" . }} - labels: - {{- include "renderer.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - {{- include "renderer.selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "renderer.selectorLabels" . | nindent 8 }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "renderer.serviceAccountName" . }} - containers: - - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - env: - - name: AFFINE_PRIVATE_KEY - valueFrom: - secretKeyRef: - name: "{{ .Values.global.secret.secretName }}" - key: key - - name: NODE_ENV - value: "{{ .Values.env }}" - - name: NODE_OPTIONS - value: "--max-old-space-size=2048" - - name: NO_COLOR - value: "1" - - name: DEPLOYMENT_TYPE - value: "{{ .Values.global.deployment.type }}" - - name: DEPLOYMENT_PLATFORM - value: "{{ .Values.global.deployment.platform }}" - - name: SERVER_FLAVOR - value: "renderer" - - name: AFFINE_ENV - value: "{{ .Release.Namespace }}" - - name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: pg-postgresql - key: postgres-password - - name: DATABASE_URL - value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }} - - name: REDIS_SERVER_ENABLED - value: "true" - - name: REDIS_SERVER_HOST - value: "{{ .Values.global.redis.host }}" - - name: REDIS_SERVER_PORT - value: "{{ .Values.global.redis.port }}" - - name: REDIS_SERVER_USER - value: "{{ .Values.global.redis.username }}" - - name: REDIS_SERVER_PASSWORD - valueFrom: - secretKeyRef: - name: redis - key: redis-password - - name: REDIS_SERVER_DATABASE - value: "{{ .Values.global.redis.database }}" - - name: AFFINE_INDEXER_SEARCH_PROVIDER - value: "{{ .Values.global.indexer.provider }}" - - name: AFFINE_INDEXER_SEARCH_ENDPOINT - value: "{{ .Values.global.indexer.endpoint }}" - - name: AFFINE_INDEXER_SEARCH_API_KEY - valueFrom: - secretKeyRef: - name: indexer - key: indexer-apiKey - - name: AFFINE_SERVER_PORT - value: "{{ .Values.service.port }}" - - name: AFFINE_SERVER_SUB_PATH - value: "{{ .Values.app.path }}" - - name: AFFINE_SERVER_HOST - value: "{{ .Values.app.host }}" - - name: AFFINE_SERVER_HTTPS - value: "{{ .Values.app.https }}" - - name: DOC_SERVICE_ENDPOINT - value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}" - ports: - - name: http - containerPort: {{ .Values.service.port }} - protocol: TCP - livenessProbe: - httpGet: - path: /info - port: http - initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} - readinessProbe: - httpGet: - path: /info - port: http - initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/.github/helm/affine/charts/renderer/templates/service.yaml b/.github/helm/affine/charts/renderer/templates/service.yaml deleted file mode 100644 index 4c34622c41..0000000000 --- a/.github/helm/affine/charts/renderer/templates/service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "graphql.fullname" . }} - labels: - {{- include "graphql.labels" . | nindent 4 }} - {{- with .Values.service.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "graphql.selectorLabels" . | nindent 4 }} diff --git a/.github/helm/affine/charts/renderer/templates/serviceaccount.yaml b/.github/helm/affine/charts/renderer/templates/serviceaccount.yaml deleted file mode 100644 index 14dac586bf..0000000000 --- a/.github/helm/affine/charts/renderer/templates/serviceaccount.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "graphql.serviceAccountName" . }} - labels: - {{- include "graphql.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/.github/helm/affine/charts/renderer/templates/tests/test-connection.yaml b/.github/helm/affine/charts/renderer/templates/tests/test-connection.yaml deleted file mode 100644 index d3b5be0e91..0000000000 --- a/.github/helm/affine/charts/renderer/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "renderer.fullname" . }}-test-connection" - labels: - {{- include "renderer.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "renderer.fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/.github/helm/affine/charts/renderer/values.yaml b/.github/helm/affine/charts/renderer/values.yaml deleted file mode 100644 index 9b9e8d3326..0000000000 --- a/.github/helm/affine/charts/renderer/values.yaml +++ /dev/null @@ -1,38 +0,0 @@ -replicaCount: 1 -image: - repository: ghcr.io/toeverything/affine - pullPolicy: IfNotPresent - tag: '' - -imagePullSecrets: [] -nameOverride: '' -fullnameOverride: '' -# map to NODE_ENV environment variable -env: 'production' -app: - # AFFINE_SERVER_SUB_PATH - path: '' - # AFFINE_SERVER_HOST - host: '0.0.0.0' - https: true -serviceAccount: - create: true - annotations: {} - name: 'affine-renderer' - -podAnnotations: {} - -podSecurityContext: - fsGroup: 2000 - -resources: - requests: - cpu: '1' - memory: 2Gi - -probe: - initialDelaySeconds: 20 - -nodeSelector: {} -tolerations: [] -affinity: {} diff --git a/.github/helm/affine/charts/sync/.helmignore b/.github/helm/affine/charts/sync/.helmignore deleted file mode 100644 index 0e8a0eb36f..0000000000 --- a/.github/helm/affine/charts/sync/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# 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/ diff --git a/.github/helm/affine/charts/sync/templates/NOTES.txt b/.github/helm/affine/charts/sync/templates/NOTES.txt deleted file mode 100644 index 2852abbb2e..0000000000 --- a/.github/helm/affine/charts/sync/templates/NOTES.txt +++ /dev/null @@ -1,16 +0,0 @@ -1. Get the application URL by running these commands: -{{- if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sync.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sync.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sync.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sync.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} diff --git a/.github/helm/affine/charts/sync/templates/deployment.yaml b/.github/helm/affine/charts/sync/templates/deployment.yaml deleted file mode 100644 index 3c0da73db4..0000000000 --- a/.github/helm/affine/charts/sync/templates/deployment.yaml +++ /dev/null @@ -1,112 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "sync.fullname" . }} - labels: - {{- include "sync.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - {{- include "sync.selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "sync.selectorLabels" . | nindent 8 }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "sync.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - env: - - name: AFFINE_PRIVATE_KEY - valueFrom: - secretKeyRef: - name: "{{ .Values.global.secret.secretName }}" - key: key - - name: NODE_ENV - value: "{{ .Values.env }}" - - name: NO_COLOR - value: "1" - - name: DEPLOYMENT_TYPE - value: "{{ .Values.global.deployment.type }}" - - name: DEPLOYMENT_PLATFORM - value: "{{ .Values.global.deployment.platform }}" - - name: SERVER_FLAVOR - value: "sync" - - name: AFFINE_ENV - value: "{{ .Release.Namespace }}" - - name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: pg-postgresql - key: postgres-password - - name: DATABASE_URL - value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }} - - name: REDIS_SERVER_HOST - value: "{{ .Values.global.redis.host }}" - - name: REDIS_SERVER_PORT - value: "{{ .Values.global.redis.port }}" - - name: REDIS_SERVER_USER - value: "{{ .Values.global.redis.username }}" - - name: REDIS_SERVER_PASSWORD - valueFrom: - secretKeyRef: - name: redis - key: redis-password - - name: REDIS_SERVER_DATABASE - value: "{{ .Values.global.redis.database }}" - - name: AFFINE_INDEXER_SEARCH_PROVIDER - value: "{{ .Values.global.indexer.provider }}" - - name: AFFINE_INDEXER_SEARCH_ENDPOINT - value: "{{ .Values.global.indexer.endpoint }}" - - name: AFFINE_INDEXER_SEARCH_API_KEY - valueFrom: - secretKeyRef: - name: indexer - key: indexer-apiKey - - name: AFFINE_SERVER_PORT - value: "{{ .Values.service.port }}" - - name: AFFINE_SERVER_HOST - value: "{{ .Values.app.host }}" - - name: DOC_SERVICE_ENDPOINT - value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}" - ports: - - name: http - containerPort: {{ .Values.service.port }} - protocol: TCP - livenessProbe: - tcpSocket: - port: http - initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} - readinessProbe: - tcpSocket: - port: http - initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/.github/helm/affine/charts/sync/templates/service.yaml b/.github/helm/affine/charts/sync/templates/service.yaml deleted file mode 100644 index 02e9cefb70..0000000000 --- a/.github/helm/affine/charts/sync/templates/service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "sync.fullname" . }} - labels: - {{- include "sync.labels" . | nindent 4 }} - {{- with .Values.service.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "sync.selectorLabels" . | nindent 4 }} diff --git a/.github/helm/affine/charts/sync/templates/serviceaccount.yaml b/.github/helm/affine/charts/sync/templates/serviceaccount.yaml deleted file mode 100644 index c03fe228a5..0000000000 --- a/.github/helm/affine/charts/sync/templates/serviceaccount.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "sync.serviceAccountName" . }} - labels: - {{- include "sync.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/.github/helm/affine/charts/sync/templates/tests/test-connection.yaml b/.github/helm/affine/charts/sync/templates/tests/test-connection.yaml deleted file mode 100644 index 59f29465a7..0000000000 --- a/.github/helm/affine/charts/sync/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "sync.fullname" . }}-test-connection" - labels: - {{- include "sync.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "sync.fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/.github/helm/affine/charts/sync/values.yaml b/.github/helm/affine/charts/sync/values.yaml deleted file mode 100644 index 11a981ba8a..0000000000 --- a/.github/helm/affine/charts/sync/values.yaml +++ /dev/null @@ -1,38 +0,0 @@ -replicaCount: 1 -image: - repository: ghcr.io/toeverything/affine - pullPolicy: IfNotPresent - tag: '' - -imagePullSecrets: [] -nameOverride: '' -fullnameOverride: '' -# map to NODE_ENV environment variable -env: 'production' -app: - # AFFINE_SERVER_HOST - host: '0.0.0.0' -serviceAccount: - create: true - annotations: {} - name: 'affine-sync' - -podAnnotations: {} - -podSecurityContext: - fsGroup: 2000 - -resources: - limits: - cpu: '2' - memory: 4Gi - requests: - cpu: '1' - memory: 2Gi - -probe: - initialDelaySeconds: 20 - -nodeSelector: {} -tolerations: [] -affinity: {} diff --git a/.github/helm/affine/charts/web/.helmignore b/.github/helm/affine/charts/web/.helmignore deleted file mode 100644 index 0e8a0eb36f..0000000000 --- a/.github/helm/affine/charts/web/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# 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/ diff --git a/.github/helm/affine/charts/web/Chart.yaml b/.github/helm/affine/charts/web/Chart.yaml deleted file mode 100644 index 8e41632a80..0000000000 --- a/.github/helm/affine/charts/web/Chart.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v2 -name: web -description: A Helm chart for Kubernetes -type: application -version: 0.0.0 -appVersion: "0.7.0-canary.18" diff --git a/.github/helm/affine/charts/web/templates/NOTES.txt b/.github/helm/affine/charts/web/templates/NOTES.txt deleted file mode 100644 index 98c2294816..0000000000 --- a/.github/helm/affine/charts/web/templates/NOTES.txt +++ /dev/null @@ -1,16 +0,0 @@ -1. Get the application URL by running these commands: -{{- if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "web.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "web.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "web.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "web.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} diff --git a/.github/helm/affine/charts/web/templates/_helpers.tpl b/.github/helm/affine/charts/web/templates/_helpers.tpl deleted file mode 100644 index 46e6f5eba3..0000000000 --- a/.github/helm/affine/charts/web/templates/_helpers.tpl +++ /dev/null @@ -1,63 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "web.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 "web.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 "web.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "web.labels" -}} -helm.sh/chart: {{ include "web.chart" . }} -{{ include "web.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -monitoring: enabled -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "web.selectorLabels" -}} -app.kubernetes.io/name: {{ include "web.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "web.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "web.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/.github/helm/affine/charts/web/templates/deployment.yaml b/.github/helm/affine/charts/web/templates/deployment.yaml deleted file mode 100644 index 7697f6a337..0000000000 --- a/.github/helm/affine/charts/web/templates/deployment.yaml +++ /dev/null @@ -1,60 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "web.fullname" . }} - labels: - {{- include "web.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - {{- include "web.selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "web.selectorLabels" . | nindent 8 }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "web.serviceAccountName" . }} - containers: - - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - env: - - name: AFFINE_ENV - value: "{{ .Release.Namespace }}" - ports: - - name: http - containerPort: {{ .Values.service.port }} - protocol: TCP - livenessProbe: - httpGet: - path: / - port: http - initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} - readinessProbe: - httpGet: - path: / - port: http - initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/.github/helm/affine/charts/web/templates/service.yaml b/.github/helm/affine/charts/web/templates/service.yaml deleted file mode 100644 index 589411f9a5..0000000000 --- a/.github/helm/affine/charts/web/templates/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "web.fullname" . }} - labels: - {{- include "web.labels" . | nindent 4 }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "web.selectorLabels" . | nindent 4 }} diff --git a/.github/helm/affine/charts/web/values.yaml b/.github/helm/affine/charts/web/values.yaml deleted file mode 100644 index 40bbffdfa6..0000000000 --- a/.github/helm/affine/charts/web/values.yaml +++ /dev/null @@ -1,37 +0,0 @@ -replicaCount: 1 - -image: - repository: ghcr.io/toeverything/affine-front - pullPolicy: IfNotPresent - tag: "" - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - create: true - annotations: {} - name: "affine-web" - -podAnnotations: {} - -podSecurityContext: - fsGroup: 2000 - -resources: - limits: - cpu: '500m' - memory: 2Gi - requests: - cpu: '500m' - memory: 2Gi - -nodeSelector: {} - -tolerations: [] - -affinity: {} - -probe: - initialDelaySeconds: 1 diff --git a/.github/helm/affine/templates/ingress.yaml b/.github/helm/affine/templates/ingress.yaml index e8af906680..1c5bbcf414 100644 --- a/.github/helm/affine/templates/ingress.yaml +++ b/.github/helm/affine/templates/ingress.yaml @@ -44,9 +44,9 @@ spec: pathType: Prefix backend: service: - name: affine-sync + name: {{ $.Values.front.services.sync.name }} port: - number: {{ $.Values.sync.service.port }} + number: {{ $.Values.front.services.sync.port }} - path: /graphql pathType: Prefix backend: @@ -65,15 +65,15 @@ spec: pathType: Prefix backend: service: - name: affine-renderer + name: {{ $.Values.front.services.renderer.name }} port: - number: {{ $.Values.renderer.service.port }} + number: {{ $.Values.front.services.renderer.port }} - path: / pathType: Prefix backend: service: - name: affine-web + name: {{ $.Values.front.services.web.name }} port: - number: {{ $.Values.web.service.port }} + number: {{ $.Values.front.services.web.port }} {{- end }} {{- end }} diff --git a/.github/helm/affine/values.yaml b/.github/helm/affine/values.yaml index e6a4ac3e91..87f3a664b5 100644 --- a/.github/helm/affine/values.yaml +++ b/.github/helm/affine/values.yaml @@ -47,27 +47,27 @@ graphql: annotations: cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' -sync: - service: - type: ClusterIP - port: 3010 - annotations: - cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' - -renderer: - service: - type: ClusterIP - port: 3000 - annotations: - cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' - doc: service: type: ClusterIP annotations: cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' -web: - service: - type: ClusterIP - port: 8080 +front: + services: + sync: + name: affine-sync + type: ClusterIP + port: 3010 + annotations: + cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' + renderer: + name: affine-renderer + type: ClusterIP + port: 3000 + annotations: + cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' + web: + name: affine-web + type: ClusterIP + port: 8080 diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index c636a824df..dd5d84fea3 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -263,18 +263,7 @@ jobs: with: app-version: ${{ inputs.app-version }} - - name: Build front Dockerfile - uses: docker/build-push-action@v6 - with: - context: . - push: true - pull: true - platforms: linux/amd64,linux/arm64 - provenance: true - file: .github/deployment/front/Dockerfile - tags: ghcr.io/toeverything/affine-front:${{inputs.build-type}}-${{ inputs.git-short-hash }} - - - name: Build graphql Dockerfile + - name: Build backend Dockerfile uses: docker/build-push-action@v6 with: context: . diff --git a/packages/backend/server/src/__tests__/e2e/apps/flavors.spec.ts b/packages/backend/server/src/__tests__/e2e/apps/flavors.spec.ts index f1ae94a3a8..e3a8959e16 100644 --- a/packages/backend/server/src/__tests__/e2e/apps/flavors.spec.ts +++ b/packages/backend/server/src/__tests__/e2e/apps/flavors.spec.ts @@ -44,3 +44,12 @@ e2e('should init renderer service', async t => { const res = await app.GET('/info').expect(200); t.is(res.body.flavor, 'renderer'); }); + +e2e('should init front service', async t => { + // @ts-expect-error override + globalThis.env.FLAVOR = 'front'; + await using app = await createApp(); + + const res = await app.GET('/info').expect(200); + t.is(res.body.flavor, 'front'); +}); diff --git a/packages/backend/server/src/__tests__/env.spec.ts b/packages/backend/server/src/__tests__/env.spec.ts index 9ad2d14575..97cecdb7b4 100644 --- a/packages/backend/server/src/__tests__/env.spec.ts +++ b/packages/backend/server/src/__tests__/env.spec.ts @@ -68,12 +68,14 @@ test('should read DEPLOYMENT_TYPE', t => { test('should read FLAVOR', t => { t.deepEqual( - ['allinone', 'graphql', 'sync', 'renderer', 'doc', 'script'].map(envVal => { - process.env.SERVER_FLAVOR = envVal; - const env = new Env(); - return env.FLAVOR; - }), - ['allinone', 'graphql', 'sync', 'renderer', 'doc', 'script'] + ['allinone', 'graphql', 'sync', 'renderer', 'front', 'doc', 'script'].map( + envVal => { + process.env.SERVER_FLAVOR = envVal; + const env = new Env(); + return env.FLAVOR; + } + ), + ['allinone', 'graphql', 'sync', 'renderer', 'front', 'doc', 'script'] ); t.throws( @@ -83,7 +85,7 @@ test('should read FLAVOR', t => { }, { message: - 'Invalid value "unknown" for environment variable SERVER_FLAVOR, expected one of ["allinone","graphql","sync","renderer","doc","script"]', + 'Invalid value "unknown" for environment variable SERVER_FLAVOR, expected one of ["allinone","graphql","sync","renderer","front","doc","script"]', } ); }); @@ -110,6 +112,7 @@ test('should tell flavors correctly', t => { graphql: true, sync: true, renderer: true, + front: false, doc: true, script: false, }); @@ -119,6 +122,17 @@ test('should tell flavors correctly', t => { graphql: true, sync: false, renderer: false, + front: false, + doc: false, + script: false, + }); + + process.env.SERVER_FLAVOR = 'front'; + t.deepEqual(new Env().flavors, { + graphql: false, + sync: false, + renderer: false, + front: true, doc: false, script: false, }); @@ -128,6 +142,7 @@ test('should tell flavors correctly', t => { graphql: false, sync: false, renderer: false, + front: false, doc: false, script: true, }); diff --git a/packages/backend/server/src/app.module.ts b/packages/backend/server/src/app.module.ts index 3ce8acc20f..b38dd1e95a 100644 --- a/packages/backend/server/src/app.module.ts +++ b/packages/backend/server/src/app.module.ts @@ -43,6 +43,7 @@ import { PermissionModule } from './core/permission'; import { QueueDashboardModule } from './core/queue-dashboard'; import { QuotaModule } from './core/quota'; import { SelfhostModule } from './core/selfhost'; +import { StaticFileModule } from './core/static-files'; import { StorageModule } from './core/storage'; import { SyncModule } from './core/sync'; import { TelemetryModule } from './core/telemetry'; @@ -173,10 +174,14 @@ export function buildAppModule(env: Env) { NotificationModule, MailModule ) - // renderer server only - .useIf(() => env.flavors.renderer, DocRendererModule) - // sync server only - .useIf(() => env.flavors.sync, SyncModule, TelemetryModule) + // renderer server and front server + .useIf(() => env.flavors.renderer || env.flavors.front, DocRendererModule) + // sync server and front server + .useIf( + () => env.flavors.sync || env.flavors.front, + SyncModule, + TelemetryModule + ) // graphql server only .useIf( () => env.flavors.graphql, @@ -199,8 +204,10 @@ export function buildAppModule(env: Env) { ) // doc service only .useIf(() => env.flavors.doc, DocServiceModule) - // self hosted server only + // worker for and self-hosted API only for self-host and local development only .useIf(() => env.dev || env.selfhosted, WorkerModule, SelfhostModule) + // static frontend routes for front flavor + .useIf(() => env.flavors.front, StaticFileModule) // gcloud .useIf(() => env.gcp, GCloudModule); diff --git a/packages/backend/server/src/core/selfhost/setup.ts b/packages/backend/server/src/core/selfhost/setup.ts index c04e5e033e..96663c149a 100644 --- a/packages/backend/server/src/core/selfhost/setup.ts +++ b/packages/backend/server/src/core/selfhost/setup.ts @@ -8,6 +8,11 @@ export class SetupMiddleware implements NestMiddleware { constructor(private readonly server: ServerService) {} use = (req: Request, res: Response, next: (error?: Error | any) => void) => { + if (!env.selfhosted) { + next(); + return; + } + // never throw this.server .initialized() diff --git a/packages/backend/server/src/core/static-files/__tests__/static.spec.ts b/packages/backend/server/src/core/static-files/__tests__/static.spec.ts new file mode 100644 index 0000000000..90924769e9 --- /dev/null +++ b/packages/backend/server/src/core/static-files/__tests__/static.spec.ts @@ -0,0 +1,193 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; + +import test from 'ava'; +import express from 'express'; +import request from 'supertest'; + +import { Namespace } from '../../../env'; +import { StaticFilesResolver } from '../static'; + +const mobileUA = + 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36'; + +function initStaticFixture(root: string) { + const staticRoot = join(root, 'static'); + + const files: Array<[string, string]> = [ + ['index.html', 'web-index'], + ['admin/index.html', 'admin-index'], + ['assets/main.js', 'web-asset'], + ['mobile/index.html', 'mobile-index'], + ['mobile/assets/main.js', 'mobile-asset'], + ]; + + for (const [file, content] of files) { + const fullPath = join(staticRoot, file); + mkdirSync(dirname(fullPath), { recursive: true }); + writeFileSync(fullPath, content); + } +} + +async function createApp(basePath = '') { + const app = express(); + const resolver = new StaticFilesResolver( + { server: { path: basePath } } as any, + { + httpAdapter: { + getInstance: () => app, + }, + } as any + ); + resolver.onModuleInit(); + return app; +} + +test.serial('serves admin files and admin route fallback', async t => { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'affine-static-files-')); + initStaticFixture(fixtureRoot); + + const prevProjectRoot = env.projectRoot; + const prevNamespace = env.NAMESPACE; + + try { + // @ts-expect-error test override + env.projectRoot = fixtureRoot; + // @ts-expect-error test override + env.NAMESPACE = Namespace.Production; + + const app = await createApp(); + + const indexRes = await request(app).get('/admin/index.html').expect(200); + t.is(indexRes.text, 'admin-index'); + + const fallbackRes = await request(app).get('/admin/settings').expect(200); + t.is(fallbackRes.text, 'admin-index'); + } finally { + // @ts-expect-error test override + env.projectRoot = prevProjectRoot; + // @ts-expect-error test override + env.NAMESPACE = prevNamespace; + rmSync(fixtureRoot, { recursive: true, force: true }); + } +}); + +test.serial( + 'serves static assets from prefixed paths and returns 404 on missing', + async t => { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'affine-static-files-')); + initStaticFixture(fixtureRoot); + + const prevProjectRoot = env.projectRoot; + const prevNamespace = env.NAMESPACE; + + try { + // @ts-expect-error test override + env.projectRoot = fixtureRoot; + // @ts-expect-error test override + env.NAMESPACE = Namespace.Production; + + const app = await createApp(); + + const assetRes = await request(app).get('/assets/main.js').expect(200); + t.is(assetRes.text, 'web-asset'); + + await request(app).get('/assets/missing.js').expect(404); + } finally { + // @ts-expect-error test override + env.projectRoot = prevProjectRoot; + // @ts-expect-error test override + env.NAMESPACE = prevNamespace; + rmSync(fixtureRoot, { recursive: true, force: true }); + } + } +); + +test.serial( + 'matches front container index behavior and cache header', + async t => { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'affine-static-files-')); + initStaticFixture(fixtureRoot); + + const prevProjectRoot = env.projectRoot; + const prevNamespace = env.NAMESPACE; + + try { + // @ts-expect-error test override + env.projectRoot = fixtureRoot; + // @ts-expect-error test override + env.NAMESPACE = Namespace.Production; + + const app = await createApp(); + + const indexRes = await request(app).get('/index.html').expect(200); + t.is(indexRes.text, 'web-index'); + t.is( + indexRes.headers['cache-control'], + 'private, no-cache, no-store, max-age=0, must-revalidate' + ); + + const fallbackRes = await request(app).get('/workspace/path').expect(200); + t.is(fallbackRes.text, 'web-index'); + } finally { + // @ts-expect-error test override + env.projectRoot = prevProjectRoot; + // @ts-expect-error test override + env.NAMESPACE = prevNamespace; + rmSync(fixtureRoot, { recursive: true, force: true }); + } + } +); + +test.serial('uses mobile root only in dev namespace for mobile UA', async t => { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'affine-static-files-')); + initStaticFixture(fixtureRoot); + + const prevProjectRoot = env.projectRoot; + const prevNamespace = env.NAMESPACE; + + try { + // @ts-expect-error test override + env.projectRoot = fixtureRoot; + // @ts-expect-error test override + env.NAMESPACE = Namespace.Dev; + + const app = await createApp(); + + const mobileAssetRes = await request(app) + .get('/assets/main.js') + .set('user-agent', mobileUA) + .expect(200); + t.is(mobileAssetRes.text, 'mobile-asset'); + + const webAssetRes = await request(app).get('/assets/main.js').expect(200); + t.is(webAssetRes.text, 'web-asset'); + + const mobileFromHint = await request(app) + .get('/assets/main.js') + .set('user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)') + .set('sec-ch-ua-mobile', '?1') + .expect(200); + t.is(mobileFromHint.text, 'mobile-asset'); + + const desktopFromHint = await request(app) + .get('/assets/main.js') + .set('user-agent', mobileUA) + .set('sec-ch-ua-mobile', '?0') + .expect(200); + t.is(desktopFromHint.text, 'web-asset'); + + const mobileFromPlatformHint = await request(app) + .get('/assets/main.js') + .set('sec-ch-ua-platform', '"Android"') + .expect(200); + t.is(mobileFromPlatformHint.text, 'mobile-asset'); + } finally { + // @ts-expect-error test override + env.projectRoot = prevProjectRoot; + // @ts-expect-error test override + env.NAMESPACE = prevNamespace; + rmSync(fixtureRoot, { recursive: true, force: true }); + } +}); diff --git a/packages/backend/server/src/core/static-files/index.ts b/packages/backend/server/src/core/static-files/index.ts new file mode 100644 index 0000000000..5f3bdc6a7b --- /dev/null +++ b/packages/backend/server/src/core/static-files/index.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; + +import { StaticFilesResolver } from './static'; + +@Module({ + providers: [StaticFilesResolver], +}) +export class StaticFileModule {} diff --git a/packages/backend/server/src/core/static-files/static.ts b/packages/backend/server/src/core/static-files/static.ts new file mode 100644 index 0000000000..fbcc31206e --- /dev/null +++ b/packages/backend/server/src/core/static-files/static.ts @@ -0,0 +1,119 @@ +import { join } from 'node:path'; + +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { HttpAdapterHost } from '@nestjs/core'; +import type { Application, Request, Response } from 'express'; +import { static as serveStatic } from 'express'; + +import { Config } from '../../base'; +import { isMobileRequest } from '../utils/user-agent'; + +const staticPathRegex = /^\/(_plugin|assets|imgs|js|plugins|static)\//; + +@Injectable() +export class StaticFilesResolver implements OnModuleInit { + constructor( + private readonly config: Config, + private readonly adapterHost: HttpAdapterHost + ) {} + + onModuleInit() { + if (!this.adapterHost.httpAdapter) { + return; + } + + const app = this.adapterHost.httpAdapter.getInstance(); + const basePath = this.config.server.path; + const rootPath = basePath || '/'; + const staticPath = join(env.projectRoot, 'static'); + const adminPath = join(staticPath, 'admin'); + const mobilePath = env.namespaces.canary + ? join(staticPath, 'mobile') + : staticPath; + + const staticAsset = serveStatic(staticPath, { + redirect: false, + index: false, + fallthrough: true, + }); + const mobileAsset = serveStatic(mobilePath, { + redirect: false, + index: false, + fallthrough: true, + }); + const staticAssetStrict = serveStatic(staticPath, { + redirect: false, + index: false, + fallthrough: false, + }); + const mobileAssetStrict = serveStatic(mobilePath, { + redirect: false, + index: false, + fallthrough: false, + }); + const adminAsset = serveStatic(adminPath, { + redirect: false, + index: false, + fallthrough: true, + }); + + const routeByUA = ( + req: Request, + res: Response, + next: (err?: unknown) => void, + strict = false + ) => { + const isMobile = isMobileRequest(req.headers); + if (strict) { + return isMobile + ? mobileAssetStrict(req, res, next) + : staticAssetStrict(req, res, next); + } + return isMobile + ? mobileAsset(req, res, next) + : staticAsset(req, res, next); + }; + + // /admin + app.use(basePath + '/admin', adminAsset); + app.get([basePath + '/admin', basePath + '/admin/*path'], (_req, res) => { + res.sendFile(join(adminPath, 'index.html')); + }); + + // /_plugin|/assets|/imgs|/js|/plugins|/static + app.use(rootPath, (req, res, next) => { + if (!staticPathRegex.test(req.path)) { + next(); + return; + } + routeByUA(req, res, next, true); + }); + + // / + app.use(rootPath, (req, res, next) => { + if (req.path.startsWith('/admin')) { + next(); + return; + } + + res.setHeader( + 'Cache-Control', + 'private, no-cache, no-store, max-age=0, must-revalidate' + ); + routeByUA(req, res, next, false); + }); + + app.get( + [basePath || '/', basePath + '/*path'], + (req: Request, res: Response) => { + if (req.path.startsWith('/admin')) { + res.status(404).end(); + return; + } + + const root = isMobileRequest(req.headers) ? mobilePath : staticPath; + res.sendFile(join(root, 'index.html')); + } + ); + } +} diff --git a/packages/backend/server/src/core/utils/__tests__/user-agent.spec.ts b/packages/backend/server/src/core/utils/__tests__/user-agent.spec.ts new file mode 100644 index 0000000000..6362ed7122 --- /dev/null +++ b/packages/backend/server/src/core/utils/__tests__/user-agent.spec.ts @@ -0,0 +1,65 @@ +import test from 'ava'; + +import { + isMobileRequest, + isMobileUserAgent, + parseRequestUserAgent, + parseUserAgent, +} from '../user-agent'; + +const mobileUserAgent = + 'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36'; +const desktopUserAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; + +test('returns desktop for empty user agent values', t => { + t.false(isMobileUserAgent(undefined)); + + t.deepEqual(parseUserAgent(undefined), { + ua: '', + deviceType: 'desktop', + isMobile: false, + }); +}); + +test('detects mobile and desktop user agents', t => { + const mobile = parseUserAgent(mobileUserAgent); + t.true(mobile.isMobile); + t.is(mobile.deviceType, 'mobile'); + + const desktop = parseUserAgent(desktopUserAgent); + t.false(desktop.isMobile); + t.is(desktop.deviceType, 'desktop'); +}); + +test('prefers sec-ch-ua-mobile over user-agent when available', t => { + const mobileFromHint = parseRequestUserAgent({ + 'user-agent': desktopUserAgent, + 'sec-ch-ua-mobile': '?1', + }); + t.true(mobileFromHint.isMobile); + t.is(mobileFromHint.deviceType, 'mobile'); + t.true(isMobileRequest({ 'sec-ch-ua-mobile': '?1' })); + + const desktopFromHint = parseRequestUserAgent({ + 'user-agent': mobileUserAgent, + 'sec-ch-ua-mobile': '?0', + }); + t.false(desktopFromHint.isMobile); + t.is(desktopFromHint.deviceType, 'desktop'); +}); + +test('uses sec-ch-ua-platform as fallback hint', t => { + const parsed = parseRequestUserAgent({ + 'sec-ch-ua-platform': '"Android"', + }); + t.true(parsed.isMobile); + t.is(parsed.deviceType, 'mobile'); + + const desktop = parseRequestUserAgent({ + 'sec-ch-ua-platform': '"Windows"', + }); + t.false(desktop.isMobile); + t.is(desktop.deviceType, 'desktop'); + t.false(isMobileUserAgent(undefined)); +}); diff --git a/packages/backend/server/src/core/utils/user-agent.ts b/packages/backend/server/src/core/utils/user-agent.ts new file mode 100644 index 0000000000..8b305c5f3f --- /dev/null +++ b/packages/backend/server/src/core/utils/user-agent.ts @@ -0,0 +1,207 @@ +import type { IncomingHttpHeaders } from 'node:http'; + +import isMobile from 'is-mobile'; + +export type UserAgentHeader = IncomingHttpHeaders['user-agent']; + +export type UserAgentDeviceType = 'desktop' | 'mobile'; + +export interface ParsedUserAgent { + readonly ua: string; + readonly deviceType: UserAgentDeviceType; + readonly isMobile: boolean; +} + +type HeaderValue = string | string[] | undefined; + +const USER_AGENT_MAX_LENGTH = 512; +const USER_AGENT_CACHE_MAX_SIZE = 1024; + +const CLIENT_HINT_MOBILE_TRUE = new Set(['?1', '1', 'true']); +const MOBILE_PLATFORM_HINTS = new Set(['android', 'ios', 'ipados']); + +const EMPTY_USER_AGENT: ParsedUserAgent = { + ua: '', + deviceType: 'desktop', + isMobile: false, +}; + +const parsedUserAgentCache = new Map(); + +interface UserAgentSignals { + ua: string; + clientHintMobile?: boolean; + clientHintPlatform?: string; +} + +function pickHeaderValue(value: HeaderValue): string { + if (typeof value === 'string') { + return value; + } + + if (!Array.isArray(value)) { + return ''; + } + + return value.find(item => typeof item === 'string' && item.length > 0) ?? ''; +} + +function normalizeHeaderValue(value: HeaderValue): string { + const header = pickHeaderValue(value); + if (!header) return ''; + return header.trim().toLowerCase(); +} + +function unquoteHeaderValue(value: string): string { + if (!value) return value; + + if (value.startsWith('"') && value.endsWith('"')) { + return value.slice(1, -1); + } + + return value; +} + +function normalizeUserAgentHeader(value: UserAgentHeader): string { + const normalized = normalizeHeaderValue(value); + if (!normalized) return ''; + if (normalized.length <= USER_AGENT_MAX_LENGTH) return normalized; + return normalized.slice(0, USER_AGENT_MAX_LENGTH); +} + +function parseClientHintMobile(value: HeaderValue): boolean | undefined { + const normalized = unquoteHeaderValue(normalizeHeaderValue(value)); + if (!normalized) return; + if (CLIENT_HINT_MOBILE_TRUE.has(normalized)) return true; + return false; +} + +function parseClientHintPlatform(value: HeaderValue): string | undefined { + const normalized = unquoteHeaderValue(normalizeHeaderValue(value)); + if (!normalized) { + return; + } + + return normalized; +} + +function parseUserAgentSignals(headers: IncomingHttpHeaders): UserAgentSignals { + return { + ua: normalizeUserAgentHeader(headers['user-agent']), + clientHintMobile: parseClientHintMobile(headers['sec-ch-ua-mobile']), + clientHintPlatform: parseClientHintPlatform(headers['sec-ch-ua-platform']), + }; +} + +function getDeviceType(signals: UserAgentSignals): UserAgentDeviceType { + if (signals.clientHintMobile !== undefined) { + return signals.clientHintMobile ? 'mobile' : 'desktop'; + } + + if ( + signals.clientHintPlatform && + MOBILE_PLATFORM_HINTS.has(signals.clientHintPlatform) + ) { + return 'mobile'; + } + + if (!signals.ua) { + return 'desktop'; + } + + const mobile = isMobile({ + ua: signals.ua, + tablet: true, + featureDetect: false, + }); + return mobile ? 'mobile' : 'desktop'; +} + +function getCacheKey(signals: UserAgentSignals): string { + const hintMobile = + signals.clientHintMobile === undefined + ? '' + : signals.clientHintMobile + ? '1' + : '0'; + const hintPlatform = signals.clientHintPlatform ?? ''; + return `${signals.ua}|${hintMobile}|${hintPlatform}`; +} + +function getCachedParsedUserAgent( + cacheKey: string +): ParsedUserAgent | undefined { + const cached = parsedUserAgentCache.get(cacheKey); + if (!cached) return; + + // Keep recently-used entries hot in the bounded cache. + parsedUserAgentCache.delete(cacheKey); + parsedUserAgentCache.set(cacheKey, cached); + return cached; +} + +function cacheParsedUserAgent( + cacheKey: string, + parsedUserAgent: ParsedUserAgent +) { + if (parsedUserAgentCache.has(cacheKey)) { + parsedUserAgentCache.delete(cacheKey); + } else if (parsedUserAgentCache.size >= USER_AGENT_CACHE_MAX_SIZE) { + const oldestUserAgent = parsedUserAgentCache.keys().next(); + if (!oldestUserAgent.done) { + parsedUserAgentCache.delete(oldestUserAgent.value); + } + } + + parsedUserAgentCache.set(cacheKey, parsedUserAgent); +} + +function parseUserAgentWithSignals(signals: UserAgentSignals): ParsedUserAgent { + const cacheKey = getCacheKey(signals); + const cached = getCachedParsedUserAgent(cacheKey); + if (cached) return cached; + + const deviceType = getDeviceType(signals); + const parsed: ParsedUserAgent = { + ua: signals.ua, + deviceType, + isMobile: deviceType === 'mobile', + }; + + cacheParsedUserAgent(cacheKey, parsed); + return parsed; +} + +export function parseUserAgent( + userAgentHeader: UserAgentHeader +): ParsedUserAgent { + const ua = normalizeUserAgentHeader(userAgentHeader); + if (!ua) { + return EMPTY_USER_AGENT; + } + + return parseUserAgentWithSignals({ ua }); +} + +export function isMobileUserAgent(userAgentHeader: UserAgentHeader): boolean { + return parseUserAgent(userAgentHeader).isMobile; +} + +export function parseRequestUserAgent( + headers: IncomingHttpHeaders +): ParsedUserAgent { + const signals = parseUserAgentSignals(headers); + if ( + !signals.ua && + signals.clientHintMobile === undefined && + !signals.clientHintPlatform + ) { + return EMPTY_USER_AGENT; + } + + return parseUserAgentWithSignals(signals); +} + +export function isMobileRequest(headers: IncomingHttpHeaders): boolean { + return parseRequestUserAgent(headers).isMobile; +} diff --git a/packages/backend/server/src/env.ts b/packages/backend/server/src/env.ts index c37ae0f51f..21a2125886 100644 --- a/packages/backend/server/src/env.ts +++ b/packages/backend/server/src/env.ts @@ -22,6 +22,7 @@ export enum Flavor { Graphql = 'graphql', Sync = 'sync', Renderer = 'renderer', + Front = 'front', Doc = 'doc', Script = 'script', } @@ -108,6 +109,7 @@ export class Env implements AppEnv { graphql: this.isFlavor(Flavor.Graphql), sync: this.isFlavor(Flavor.Sync), renderer: this.isFlavor(Flavor.Renderer), + front: this.FLAVOR === Flavor.Front, doc: this.isFlavor(Flavor.Doc), // Script in a special flavor, return true only when it is set explicitly script: this.FLAVOR === Flavor.Script, diff --git a/scripts/set-version.sh b/scripts/set-version.sh index 4c9b2dea14..1d81682e68 100755 --- a/scripts/set-version.sh +++ b/scripts/set-version.sh @@ -103,8 +103,7 @@ ios_new_version=${IOS_APP_VERSION:-$new_version} update_app_version_in_helm_charts ".github/helm/affine/Chart.yaml" "$new_version" update_app_version_in_helm_charts ".github/helm/affine/charts/graphql/Chart.yaml" "$new_version" -update_app_version_in_helm_charts ".github/helm/affine/charts/sync/Chart.yaml" "$new_version" -update_app_version_in_helm_charts ".github/helm/affine/charts/renderer/Chart.yaml" "$new_version" +update_app_version_in_helm_charts ".github/helm/affine/charts/front/Chart.yaml" "$new_version" update_app_version_in_helm_charts ".github/helm/affine/charts/doc/Chart.yaml" "$new_version" update_app_stream_version "packages/frontend/apps/electron/resources/affine.metainfo.xml" "$new_version"