Compare commits

...

7 Commits

Author SHA1 Message Date
lebaudantoine
302e5a503f wip introduce convert logic 2024-10-21 00:57:08 +02:00
lebaudantoine
a1c497ef27 wip install prettier and format 2024-10-21 00:52:31 +02:00
lebaudantoine
89a44e4979 add blocknote micro service to kube 2024-10-20 23:49:25 +02:00
lebaudantoine
993394d91f add the image to the tilt stack 2024-10-20 23:48:05 +02:00
lebaudantoine
8f386402e8 add the image to the compose stack 2024-10-20 23:16:04 +02:00
lebaudantoine
c7c252f9a1 wip containerized the app 2024-10-20 23:15:37 +02:00
lebaudantoine
5c0e7b9043 wip bootstrap an express app 2024-10-20 23:15:20 +02:00
15 changed files with 5551 additions and 1 deletions

View File

@@ -39,6 +39,18 @@ docker_build(
]
)
docker_build(
'localhost:5001/impress-blocknote:latest',
context='..',
dockerfile='../src/blocknote/Dockerfile',
only=['./src/blocknote', './docker', './.dockerignore'],
target = 'production',
live_update=[
sync('../src/blocknote', '/home/blocknote'),
]
)
k8s_yaml(local('cd ../src/helm && helmfile -n impress -e dev template .'))
migration = '''

View File

@@ -134,6 +134,16 @@ services:
ports:
- "3000:3000"
blocknote-converter:
user: "${DOCKER_USER:-1000}"
build:
context: .
dockerfile: ./src/blocknote/Dockerfile
target: production
image: blocknote:blocknote-production
ports:
- "8081:8081"
dockerize:
image: jwilder/dockerize

View File

@@ -0,0 +1,5 @@
{
"semi": false,
"trailingComma": "es5",
"singleQuote": true
}

28
src/blocknote/Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
FROM node:20-alpine AS dependencies
WORKDIR /home/blocknote
COPY ./src/blocknote/package*.json ./
RUN npm install
COPY .dockerignore ./.dockerignore
COPY ./src/blocknote/ .
FROM dependencies AS blocknote-builder
WORKDIR /home/blocknote
RUN npm run build
# ---- Blocknote image ----
FROM blocknote-builder AS production
# Un-privileged user running the application
ARG DOCKER_USER
USER ${DOCKER_USER}
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint"]
CMD ["npm", "run", "start"]

View File

@@ -0,0 +1,5 @@
{
"watch": ["src"],
"ext": "ts",
"exec": "concurrently \"npx tsc --watch\" \"ts-node src/index.ts\""
}

5084
src/blocknote/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
{
"name": "blocknote-server",
"version": "1.0.0",
"license": "MIT",
"main": "dist/index.js",
"keywords": [
"nodejs",
"bootstrap",
"express"
],
"scripts": {
"build": "npx tsc",
"start": "node dist/index.js",
"dev": "nodemon src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --write ./src",
"check": "prettier --check ./src"
},
"dependencies": {
"@blocknote/server-util": "0.17.1",
"dotenv": "16.4.5",
"express": "4.21.1",
"prettier": "3.3.3",
"yjs": "13.6.20"
},
"devDependencies": {
"@types/express": "5.0.0",
"@types/node": "22.7.7",
"concurrently": "9.0.1",
"nodemon": "3.1.7",
"ts-node": "10.9.2",
"typescript": "5.6.3",
"prettier": "3.3.3"
}
}

View File

@@ -0,0 +1,37 @@
import express, { Express, Request, Response } from 'express'
import { asyncWrapper, convertMarkdown } from './utils'
import dotenv from 'dotenv'
import bodyParser from 'body-parser'
dotenv.config()
const app: Express = express()
const router = express.Router()
const port = process.env.PORT ?? 8081
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
// Logging middleware, logs the request method and path for each incoming request
router.use(async function (req, res, next) {
console.log(`/${req.method}`)
next()
})
// Liveness probe endpoint for Kubernetes health checks
router.get('/__heartbeat__', (req: Request, res: Response) => {
res.status(200).send({ status: 'OK' })
})
// Load balancer heartbeat check, useful to detect app readiness
router.get('/__lbheartbeat__', (req: Request, res: Response) => {
res.status(200).send({ status: 'OK' })
})
router.post('/', asyncWrapper(convertMarkdown))
app.use('/', router)
app.listen(port, () => {
console.log(`[server]: Server listening on port ${port}`)
})

View File

@@ -0,0 +1,60 @@
// Utility functions for handling markdown conversion and related operations
import { NextFunction, Request, Response } from 'express'
import { ServerBlockNoteEditor } from '@blocknote/server-util'
import Y from 'yjs'
const toBase64 = function (str: Uint8Array) {
return Buffer.from(str).toString('base64')
}
export const asyncWrapper = (
asyncFn: (req: Request, res: Response) => Promise<Response>
) => {
return function (req: Request, res: Response, next: NextFunction) {
asyncFn(req, res).catch(next)
}
}
const validateContent = (content: string | undefined): string => {
if (!content) {
throw new Error('Content is required')
}
return content
}
const parseMarkdownToBlocks = async (
blockNoteEditor: ServerBlockNoteEditor,
content: string
) => {
try {
const blocks = await blockNoteEditor.tryParseMarkdownToBlocks(content)
if (!blocks || blocks.length === 0) {
throw new Error('No valid blocks generated')
}
return blocks
} catch (error) {
throw new Error('Failed to parse markdown content')
}
}
const processContentBlocks = (server: ServerBlockNoteEditor, blocks: any[]) => {
try {
const yDocument = server.blocksToYDoc(blocks, 'document-store')
return toBase64(Y.encodeStateAsUpdate(yDocument))
} catch (error) {
throw new Error('Failed to process content blocks')
}
}
export const convertMarkdown = async (req: Request, res: Response) => {
try {
const content = validateContent(req.body.content)
const editor = ServerBlockNoteEditor.create()
const blocks = await parseMarkdownToBlocks(editor, content)
const encodedContent = processContentBlocks(editor, blocks)
return res.send({ content: encodedContent })
} catch (error) {
return res.status(500).json({ error: (error as Error).message })
}
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

View File

@@ -93,6 +93,14 @@ yProvider:
pullPolicy: Always
tag: "latest"
blocknote:
replicas: 1
image:
repository: localhost:5001/impress-blocknote
pullPolicy: Always
tag: "latest"
ingress:
enabled: true
host: impress.127.0.0.1.nip.io

View File

@@ -157,6 +157,15 @@ Requires top level scope
{{ include "impress.fullname" . }}-y-provider
{{- end }}
{{/*
Full name for the blocknote
Requires top level scope
*/}}
{{- define "impress.blocknote.fullname" -}}
{{ include "impress.fullname" . }}-blocknote
{{- end }}
{{/*
Usage : {{ include "impress.secret.dockerconfigjson.name" (dict "fullname" (include "impress.fullname" .) "imageCredentials" .Values.path.to.the.image1) }}
*/}}

View File

@@ -0,0 +1,136 @@
{{- $envVars := include "impress.common.env" (list . .Values.blocknote) -}}
{{- $fullName := include "impress.blocknote.fullname" . -}}
{{- $component := "blocknote" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "impress.common.labels" (list . $component) | nindent 4 }}
spec:
replicas: {{ .Values.blocknote.replicas }}
selector:
matchLabels:
{{- include "impress.common.selectorLabels" (list . $component) | nindent 6 }}
template:
metadata:
annotations:
{{- with .Values.blocknote.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "impress.common.selectorLabels" (list . $component) | nindent 8 }}
spec:
{{- if $.Values.image.credentials }}
imagePullSecrets:
- name: {{ include "impress.secret.dockerconfigjson.name" (dict "fullname" (include "impress.fullname" .) "imageCredentials" $.Values.image.credentials) }}
{{- end}}
shareProcessNamespace: {{ .Values.blocknote.shareProcessNamespace }}
containers:
{{- with .Values.blocknote.sidecars }}
{{- toYaml . | nindent 8 }}
{{- end }}
- name: {{ .Chart.Name }}
image: "{{ (.Values.blocknote.image | default dict).repository | default .Values.image.repository }}:{{ (.Values.blocknote.image | default dict).tag | default .Values.image.tag }}"
imagePullPolicy: {{ (.Values.blocknote.image | default dict).pullPolicy | default .Values.image.pullPolicy }}
{{- with .Values.blocknote.command }}
command:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.blocknote.args }}
args:
{{- toYaml . | nindent 12 }}
{{- end }}
env:
{{- if $envVars}}
{{- $envVars | indent 12 }}
{{- end }}
{{- with .Values.blocknote.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.blocknote.service.targetPort }}
protocol: TCP
{{- if .Values.blocknote.probes.liveness }}
livenessProbe:
{{- include "impress.probes.abstract" (merge .Values.blocknote.probes.liveness (dict "targetPort" .Values.blocknote.service.targetPort )) | nindent 12 }}
{{- end }}
{{- if .Values.blocknote.probes.readiness }}
readinessProbe:
{{- include "impress.probes.abstract" (merge .Values.blocknote.probes.readiness (dict "targetPort" .Values.blocknote.service.targetPort )) | nindent 12 }}
{{- end }}
{{- if .Values.blocknote.probes.startup }}
startupProbe:
{{- include "impress.probes.abstract" (merge .Values.blocknote.probes.startup (dict "targetPort" .Values.blocknote.service.targetPort )) | nindent 12 }}
{{- end }}
{{- with .Values.blocknote.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
{{- range $index, $value := .Values.mountFiles }}
- name: "files-{{ $index }}"
mountPath: {{ $value.path }}
subPath: content
{{- end }}
{{- range $name, $volume := .Values.blocknote.persistence }}
- name: "{{ $name }}"
mountPath: "{{ $volume.mountPath }}"
{{- end }}
{{- range .Values.blocknote.extraVolumeMounts }}
- name: {{ .name }}
mountPath: {{ .mountPath }}
subPath: {{ .subPath | default "" }}
readOnly: {{ .readOnly }}
{{- end }}
{{- with .Values.blocknote.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.blocknote.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.blocknote.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
{{- range $index, $value := .Values.mountFiles }}
- name: "files-{{ $index }}"
configMap:
name: "{{ include "impress.fullname" $ }}-files-{{ $index }}"
{{- end }}
{{- range $name, $volume := .Values.blocknote.persistence }}
- name: "{{ $name }}"
{{- if eq $volume.type "emptyDir" }}
emptyDir: {}
{{- else }}
persistentVolumeClaim:
claimName: "{{ $fullName }}-{{ $name }}"
{{- end }}
{{- end }}
{{- range .Values.blocknote.extraVolumes }}
- name: {{ .name }}
{{- if .existingClaim }}
persistentVolumeClaim:
claimName: {{ .existingClaim }}
{{- else if .hostPath }}
hostPath:
{{ toYaml .hostPath | nindent 12 }}
{{- else if .csi }}
csi:
{{- toYaml .csi | nindent 12 }}
{{- else if .configMap }}
configMap:
{{- toYaml .configMap | nindent 12 }}
{{- else if .emptyDir }}
emptyDir:
{{- toYaml .emptyDir | nindent 12 }}
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,21 @@
{{- $envVars := include "impress.common.env" (list . .Values.blocknote) -}}
{{- $fullName := include "impress.blocknote.fullname" . -}}
{{- $component := "blocknote" -}}
apiVersion: v1
kind: Service
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "impress.common.labels" (list . $component) | nindent 4 }}
annotations:
{{- toYaml $.Values.blocknote.service.annotations | nindent 4 }}
spec:
type: {{ .Values.blocknote.service.type }}
ports:
- port: {{ .Values.blocknote.service.port }}
targetPort: {{ .Values.blocknote.service.targetPort }}
protocol: TCP
name: http
selector:
{{- include "impress.common.selectorLabels" (list . $component) | nindent 4 }}

View File

@@ -412,4 +412,93 @@ yProvider:
extraVolumeMounts: []
## @param yProvider.extraVolumes Additional volumes to mount on the yProvider.
extraVolumes: []
extraVolumes: []
## @section blocknote
blocknote:
## @param blocknote.command Override the blocknote container command
command: []
## @param blocknote.args Override the blocknote container args
args: []
## @param blocknote.replicas Amount of blocknote replicas
replicas: 3
## @param blocknote.shareProcessNamespace Enable share process namespace between containers
shareProcessNamespace: false
## @param blocknote.sidecars Add sidecars containers to blocknote deployment
sidecars: []
## @param blocknote.securityContext Configure blocknote Pod security context
securityContext: null
## @param blocknote.envVars Configure blocknote container environment variables
## @extra blocknote.envVars.BY_VALUE Example environment variable by setting value directly
## @extra blocknote.envVars.FROM_CONFIGMAP.configMapKeyRef.name Name of a ConfigMap when configuring env vars from a ConfigMap
## @extra blocknote.envVars.FROM_CONFIGMAP.configMapKeyRef.key Key within a ConfigMap when configuring env vars from a ConfigMap
## @extra blocknote.envVars.FROM_SECRET.secretKeyRef.name Name of a Secret when configuring env vars from a Secret
## @extra blocknote.envVars.FROM_SECRET.secretKeyRef.key Key within a Secret when configuring env vars from a Secret
## @skip blocknote.envVars
envVars:
<<: *commonEnvVars
## @param blocknote.podAnnotations Annotations to add to the blocknote Pod
podAnnotations: {}
## @param blocknote.service.type blocknote Service type
## @param blocknote.service.port blocknote Service listening port
## @param blocknote.service.targetPort blocknote container listening port
## @param blocknote.service.annotations Annotations to add to the blocknote Service
service:
type: ClusterIP
port: 80
targetPort: 8081
annotations: {}
## @param blocknote.probes.liveness.path [nullable] Configure path for blocknote HTTP liveness probe
## @param blocknote.probes.liveness.targetPort [nullable] Configure port for blocknote HTTP liveness probe
## @param blocknote.probes.liveness.initialDelaySeconds [nullable] Configure initial delay for blocknote liveness probe
## @param blocknote.probes.liveness.initialDelaySeconds [nullable] Configure timeout for blocknote liveness probe
## @param blocknote.probes.startup.path [nullable] Configure path for blocknote HTTP startup probe
## @param blocknote.probes.startup.targetPort [nullable] Configure port for blocknote HTTP startup probe
## @param blocknote.probes.startup.initialDelaySeconds [nullable] Configure initial delay for blocknote startup probe
## @param blocknote.probes.startup.initialDelaySeconds [nullable] Configure timeout for blocknote startup probe
## @param blocknote.probes.readiness.path [nullable] Configure path for blocknote HTTP readiness probe
## @param blocknote.probes.readiness.targetPort [nullable] Configure port for blocknote HTTP readiness probe
## @param blocknote.probes.readiness.initialDelaySeconds [nullable] Configure initial delay for blocknote readiness probe
## @param blocknote.probes.readiness.initialDelaySeconds [nullable] Configure timeout for blocknote readiness probe
probes:
liveness:
path: /__heartbeat__
initialDelaySeconds: 10
readiness:
path: /__lbheartbeat__
initialDelaySeconds: 10
## @param blocknote.resources Resource requirements for the blocknote container
resources: {}
## @param blocknote.nodeSelector Node selector for the blocknote Pod
nodeSelector: {}
## @param blocknote.tolerations Tolerations for the blocknote Pod
tolerations: []
## @param blocknote.affinity Affinity for the blocknote Pod
affinity: {}
## @param blocknote.persistence Additional volumes to create and mount on the blocknote. Used for debugging purposes
## @extra blocknote.persistence.volume-name.size Size of the additional volume
## @extra blocknote.persistence.volume-name.type Type of the additional volume, persistentVolumeClaim or emptyDir
## @extra blocknote.persistence.volume-name.mountPath Path where the volume should be mounted to
persistence: {}
## @param blocknote.extraVolumeMounts Additional volumes to mount on the blocknote.
extraVolumeMounts: []
## @param blocknote.extraVolumes Additional volumes to mount on the blocknote.
extraVolumes: []