diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fb56d69 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +# .env - Should not be added because it contains private keys used for encryption. Instead, provide env variables to the docker container. +.env + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..118f188 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,30 @@ +# name: Deploy +# on: +# push: +# branches: +# - main + +# jobs: +# docker-image: +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v4 +# - uses: serlo/configure-repositories/actions/setup-node@main +# - uses: google-github-actions/auth@v2 +# with: +# credentials_json: '${{ secrets.GCP_KEY_CONTAINER_REGISTRY }}' +# - run: gcloud auth configure-docker +# - uses: google-github-actions/setup-gcloud@v2 +# - run: yarn push-image:latest +# deploy-image: +# runs-on: ubuntu-latest +# needs: docker-image +# steps: +# - uses: google-github-actions/auth@v2 +# with: +# credentials_json: '${{ secrets.GCP_STAGING_SERVICE_ACCOUNT }}' +# - uses: google-github-actions/get-gke-credentials@v2 +# with: +# cluster_name: serlo-staging-cluster +# location: europe-west3-a +# - run: kubectl delete pod -n editor -l app=editor-as-lti-tool diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..7a2ee36 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,34 @@ +# name: Docker +# on: +# pull_request: +# merge_group: +# jobs: +# build-docker-images: +# name: Build docker images +# runs-on: ubuntu-latest +# strategy: +# fail-fast: false +# matrix: +# imagetag: [dev, latest] +# include: +# - imagetag: dev +# dockerfile: Dockerfile.dev +# - imagetag: latest +# dockerfile: Dockerfile +# steps: +# - uses: actions/checkout@v4 +# - name: Set up Docker Buildx +# uses: docker/setup-buildx-action@v3 +# - name: Build the ${{ matrix.imagetag }} image +# uses: docker/build-push-action@v5 +# with: +# push: false +# load: true +# file: ${{ matrix.dockerfile }} +# tags: local-build:${{ matrix.imagetag }} +# - uses: serlo/configure-repositories/actions/setup-node@main +# - name: Run the ${{ matrix.imagetag }} docker image +# run: yarn docker:run local-build:${{ matrix.imagetag }} +# # TODO +# # - name: Test the docker container ${{ matrix.imagetag }} +# # run: yarn test:docker diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 index 85b738b..0000000 --- a/.yarnrc +++ /dev/null @@ -1,5 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -yarn-path ".yarn/releases/yarn-1.22.22.cjs" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ee90229 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# TODO: when this repo build first the src code, change to only run dist folders + +FROM node:20.13-alpine +WORKDIR /app + +COPY tsconfig.json tsconfig.node.json \ + vite.config.ts index.html \ + package.json yarn.lock ./ +COPY src src +RUN yarn --immutable --immutable-cache --silent +RUN yarn build + +EXPOSE 3000 +ENTRYPOINT ["yarn", "start"] \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..3f8d561 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,11 @@ +FROM node:20.13 +WORKDIR /app + +# maybe easier to git clone the repo +COPY . . + +RUN apt update && apt install neovim nano -y +RUN yarn --immutable --immutable-cache --silent +RUN yarn build + +ENTRYPOINT ["yarn", "start:dev"] \ No newline at end of file diff --git a/package.json b/package.json index 7c40af9..c9a999f 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,13 @@ "license": "Apache-2.0", "scripts": { "build": "tsc && vite build", + "docker:run": "docker run -p 3000:3000 -d --env-file .env-template --add-host=host.docker.internal:host-gateway", "start": "node --loader=ts-node/esm src/backend/index.ts", + "start:dev": "nodemon --watch src --ext ts,tsx -exec 'node --loader=ts-node/esm src/backend/index.ts' --delay 3", "lint": "tsc --noEmit", + "push-image": "yarn node --loader=ts-node/esm push-image.ts", + "push-image:dev": "yarn push-image dev", + "push-image:latest": "yarn push-image latest", "start:mongodb": "sudo systemctl start mongod", "stop:mongodb": "sudo systemctl stop mongod", "helper:resolve-mongodb-issue": "sudo rm -rf /tmp/mongodb-27017.sock", @@ -39,6 +44,7 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", + "nodemon": "^3.1.3", "vite": "^5.2.0" }, "packageManager": "yarn@4.2.2" diff --git a/push-image.ts b/push-image.ts new file mode 100644 index 0000000..e26475a --- /dev/null +++ b/push-image.ts @@ -0,0 +1,75 @@ +import * as t from "io-ts"; +import { spawnSync } from "node:child_process"; + + +enum ImageTag { + Dev = "dev", + Latest = "latest", +} + +void run(); + +function run() { + const imageTag = process.argv[2]; + + if (!imageTag) { + throw new Error( + `You have to specify image tag, ${ImageTag.Dev} or ${ImageTag.Latest}` + ); + } + if ( + !t.union([t.literal(ImageTag.Dev), t.literal(ImageTag.Latest)]).is(imageTag) + ) { + throw new Error( + `Invalid environment name, please use ${ImageTag.Dev} or ${ImageTag.Latest}` + ); + } + buildDockerImage({ + name: "editor-as-lti-tool", + imageTag, + }); +} + +function buildDockerImage({ + name, + imageTag, +}: { + name: string; + imageTag: ImageTag; +}) { + const remoteName = `eu.gcr.io/serlo-shared/${name}`; + const date = new Date(); + const timestamp = `${date.toISOString().split("T")[0]}-${date.getTime()}`; + + const { stdout: gitHashBuffer } = spawnSync("git", [ + "rev-parse", + "--short", + "HEAD", + ]); + + const remoteTags = toTags(remoteName, [ + imageTag, + timestamp, + gitHashBuffer.toString().split("\n")[0], + ]); + const tags = [...remoteTags, ...toTags(name, [imageTag])]; + + const dockerfile = imageTag == ImageTag.Dev ? ["-f", "Dockerfile.dev"] : []; + + spawnSync( + "docker", + ["build", ...dockerfile, ...tags.flatMap((tag) => ["-t", tag]), "."], + { stdio: "inherit" } + ); + + remoteTags.forEach((remoteTag) => { + // eslint-disable-next-line no-console + console.log("Pushing", remoteTag); + spawnSync("docker", ["push", remoteTag], { stdio: "inherit" }); + }); +} + +function toTags(name: string, versions: string[]) { + return versions.map((version) => `${name}:${version}`); +} + diff --git a/src/backend/index.ts b/src/backend/index.ts index 7044869..175417d 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -4,6 +4,7 @@ import path from "path"; import * as t from "io-ts"; import * as jwt from "jsonwebtoken"; +// Requires Node.js 20.11 or higher const __dirname = import.meta.dirname; const ltijsKey = readEnvVariable("LTIJS_KEY"); diff --git a/yarn.lock b/yarn.lock index 9286853..629dd09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1105,6 +1105,16 @@ __metadata: languageName: node linkType: hard +"anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10/3e044fd6d1d26545f235a9fe4d7a534e2029d8e59fa7fd9f2a6eb21230f6b5380ea1eaf55136e60cbf8e613544b3b766e7a6fa2102e2a3a117505466e3025dc2 + languageName: node + linkType: hard + "arg@npm:^4.1.0": version: 4.1.3 resolution: "arg@npm:4.1.3" @@ -1140,6 +1150,13 @@ __metadata: languageName: node linkType: hard +"binary-extensions@npm:^2.0.0": + version: 2.3.0 + resolution: "binary-extensions@npm:2.3.0" + checksum: 10/bcad01494e8a9283abf18c1b967af65ee79b0c6a9e6fcfafebfe91dbe6e0fc7272bafb73389e198b310516ae04f7ad17d79aacf6cb4c0d5d5202a7e2e52c7d98 + languageName: node + linkType: hard + "body-parser@npm:1.20.2, body-parser@npm:^1.20.2": version: 1.20.2 resolution: "body-parser@npm:1.20.2" @@ -1179,7 +1196,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.3": +"braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -1281,6 +1298,25 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^3.5.2": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10/c327fb07704443f8d15f7b4a7ce93b2f0bc0e6cea07ec28a7570aa22cd51fcf0379df589403976ea956c369f25aa82d84561947e227cd925902e1751371658df + languageName: node + linkType: hard + "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -1430,6 +1466,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4": + version: 4.3.5 + resolution: "debug@npm:4.3.5" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/cb6eab424c410e07813ca1392888589972ce9a32b8829c6508f5e1f25f3c3e70a76731610ae55b4bbe58d1a2fffa1424b30e97fa8d394e49cd2656a9643aedd2 + languageName: node + linkType: hard + "decompress-response@npm:^6.0.0": version: 6.0.0 resolution: "decompress-response@npm:6.0.0" @@ -2092,7 +2140,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.2": +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -2204,6 +2252,13 @@ __metadata: languageName: node linkType: hard +"has-flag@npm:^3.0.0": + version: 3.0.0 + resolution: "has-flag@npm:3.0.0" + checksum: 10/4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b + languageName: node + linkType: hard + "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -2318,6 +2373,13 @@ __metadata: languageName: node linkType: hard +"ignore-by-default@npm:^1.0.1": + version: 1.0.1 + resolution: "ignore-by-default@npm:1.0.1" + checksum: 10/441509147b3615e0365e407a3c18e189f78c07af08564176c680be1fabc94b6c789cad1342ad887175d4ecd5225de86f73d376cec8e06b42fd9b429505ffcf8a + languageName: node + linkType: hard + "ignore@npm:^5.2.0, ignore@npm:^5.3.1": version: 5.3.1 resolution: "ignore@npm:5.3.1" @@ -2392,6 +2454,15 @@ __metadata: languageName: node linkType: hard +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: 10/078e51b4f956c2c5fd2b26bb2672c3ccf7e1faff38e0ebdba45612265f4e3d9fc3127a1fa8370bbf09eab61339203c3d3b7af5662cbf8be4030f8fac37745b0e + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -2406,7 +2477,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -3044,6 +3115,26 @@ __metadata: languageName: node linkType: hard +"nodemon@npm:^3.1.3": + version: 3.1.3 + resolution: "nodemon@npm:3.1.3" + dependencies: + chokidar: "npm:^3.5.2" + debug: "npm:^4" + ignore-by-default: "npm:^1.0.1" + minimatch: "npm:^3.1.2" + pstree.remy: "npm:^1.1.8" + semver: "npm:^7.5.3" + simple-update-notifier: "npm:^2.0.0" + supports-color: "npm:^5.5.0" + touch: "npm:^3.1.0" + undefsafe: "npm:^2.0.5" + bin: + nodemon: bin/nodemon.js + checksum: 10/ad55e76bb755c07b07c05b1b4ea7aca7bdf71bf8c7bfbf2db95e8ddcc964dde5eab9599c9de0db6899db9f2b98e573220f2d3c0796b41865ae754409e557cf50 + languageName: node + linkType: hard + "nopt@npm:^7.0.0": version: 7.2.1 resolution: "nopt@npm:7.2.1" @@ -3055,6 +3146,13 @@ __metadata: languageName: node linkType: hard +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: 10/88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20 + languageName: node + linkType: hard + "normalize-url@npm:^6.0.1": version: 6.1.0 resolution: "normalize-url@npm:6.1.0" @@ -3219,7 +3317,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.3.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10/60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc @@ -3278,6 +3376,13 @@ __metadata: languageName: node linkType: hard +"pstree.remy@npm:^1.1.8": + version: 1.1.8 + resolution: "pstree.remy@npm:1.1.8" + checksum: 10/ef13b1b5896b35f67dbd4fb7ba54bb2a5da1a5c317276cbad4bcad4159bf8f7b5e1748dc244bf36865f3d560d2fc952521581280a91468c9c2df166cc760c8c1 + languageName: node + linkType: hard + "pump@npm:^3.0.0": version: 3.0.0 resolution: "pump@npm:3.0.0" @@ -3374,6 +3479,15 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 10/196b30ef6ccf9b6e18c4e1724b7334f72a093d011a99f3b5920470f0b3406a51770867b3e1ae9711f227ef7a7065982f6ee2ce316746b2cb42c88efe44297fe7 + languageName: node + linkType: hard + "regenerator-runtime@npm:^0.14.0": version: 0.14.1 resolution: "regenerator-runtime@npm:0.14.1" @@ -3524,7 +3638,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.5, semver@npm:^7.5.4, semver@npm:^7.6.0": +"semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.6.2 resolution: "semver@npm:7.6.2" bin: @@ -3576,6 +3690,7 @@ __metadata: io-ts: "npm:^2.2.21" jsonwebtoken: "npm:9.0.2" ltijs: "npm:^5.9.5" + nodemon: "npm:^3.1.3" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" ts-node: "npm:^10.9.2" @@ -3659,6 +3774,15 @@ __metadata: languageName: node linkType: hard +"simple-update-notifier@npm:^2.0.0": + version: 2.0.0 + resolution: "simple-update-notifier@npm:2.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10/40bd4f96aa89aedbf717ae9f4ab8fca70e8f7511e8b766feb15471cca3f6fe4fe673743309b08b4ba8abfe0965c9cd927e1de46550a757b819b70fc7430cc85d + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -3780,6 +3904,15 @@ __metadata: languageName: node linkType: hard +"supports-color@npm:^5.5.0": + version: 5.5.0 + resolution: "supports-color@npm:5.5.0" + dependencies: + has-flag: "npm:^3.0.0" + checksum: 10/5f505c6fa3c6e05873b43af096ddeb22159831597649881aeb8572d6fe3b81e798cc10840d0c9735e0026b250368851b7f77b65e84f4e4daa820a4f69947f55b + languageName: node + linkType: hard + "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -3826,6 +3959,15 @@ __metadata: languageName: node linkType: hard +"touch@npm:^3.1.0": + version: 3.1.1 + resolution: "touch@npm:3.1.1" + bin: + nodetouch: bin/nodetouch.js + checksum: 10/853e763a1f4903302c5654ed353f84ad85baf757dac62c2d37ab67e0477cfd271e8c64771fcfad42310aff7c9d284ddb435ee5ca13ff36d0f3693fedd8e971d1 + languageName: node + linkType: hard + "tr46@npm:^3.0.0": version: 3.0.0 resolution: "tr46@npm:3.0.0" @@ -3928,6 +4070,13 @@ __metadata: languageName: node linkType: hard +"undefsafe@npm:^2.0.5": + version: 2.0.5 + resolution: "undefsafe@npm:2.0.5" + checksum: 10/f42ab3b5770fedd4ada175fc1b2eb775b78f609156f7c389106aafd231bfc210813ee49f54483d7191d7b76e483bc7f537b5d92d19ded27156baf57592eb02cc + languageName: node + linkType: hard + "undici-types@npm:~5.26.4": version: 5.26.5 resolution: "undici-types@npm:5.26.5"