Compare commits

..

8 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
Anthony LC
e35671c450 📝(docs) add CONTRIBUTING.md doc
Add a CONTRIBUTING.md file to the project root
to help new contributors understand how to
contribute to the project.
2024-10-18 09:33:38 +02:00
17 changed files with 5609 additions and 1 deletions

View File

@@ -9,6 +9,10 @@ and this project adheres to
## [Unreleased]
## Added
- 📝Contributing.md #352
## [1.6.0] - 2024-10-17

54
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,54 @@
# Contributing to the Project
Thank you for taking the time to contribute! Please follow these guidelines to ensure a smooth and productive workflow. 🚀🚀🚀
To get started with the project, please refer to the [README.md](https://github.com/numerique-gouv/impress/blob/main/README.md) for detailed instructions.
## Creating an Issue
When creating an issue, please provide the following details:
1. **Title**: A concise and descriptive title for the issue.
2. **Description**: A detailed explanation of the issue, including relevant context or screenshots if applicable.
3. **Steps to Reproduce**: If the issue is a bug, include the steps needed to reproduce the problem.
4. **Expected vs. Actual Behavior**: Describe what you expected to happen and what actually happened.
5. **Labels**: Add appropriate labels to categorize the issue (e.g., bug, feature request, documentation).
## Commit Message Format
All commit messages must adhere to the following format:
`<gitmoji>(type) title description`
* **<gitmoji>**: Use a gitmoji to represent the purpose of the commit. For example, ✨ for adding a new feature or 🔥 for removing something, see the list here: <https://gitmoji.dev/>.
* **(type)**: Describe the type of change. Common types include `backend`, `frontend`, `CI`, `docker` etc...
* **title**: A short, descriptive title for the change, starting with a lowercase character.
* **description**: Include additional details about what was changed and why.
### Example Commit Message
```
✨(frontend) add user authentication logic
Implemented login and signup features, and integrated OAuth2 for social login.
```
## Pull Requests
It is nice to add information about the purpose of the pull request to help reviewers understand the context and intent of the changes.
Once all the required tests have passed, you can request a review from the project maintainers.
## Code Style
Please maintain consistency in code style. Run any linting tools available to make sure the code is clean and follows the project's conventions.
## Tests
Make sure that all new features or fixes have corresponding tests. Run the test suite before pushing your changes to ensure that nothing is broken.
## Asking for Help
If you need any help while contributing, feel free to open a discussion or ask for guidance in the issue tracker. We are more than happy to assist!
Thank you for your contributions! 👍

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: []