diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b8fd49b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +# Node modules +node_modules +npm-debug.log +yarn-error.log + +# Logs +logs +*.log +npm-debug.log* + +# Environment variables +.env +.env.* + +# Docker files +Dockerfile +docker-compose.yml +.dockerignore + +# Git files +.git +.gitignore + +# Others +dist +coverage \ No newline at end of file diff --git a/.env.example b/.env.example index 6e661bc..892a972 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,13 @@ NODE_ENV=development - PORT=3000 -DATABASE_URL="postgresql://postgres@localhost:5432/planifetsDB?schema=public" +# Database Configuration +DATABASE_URL="postgresql://postgres@localhost:5433/planifetsDB?schema=public" +POSTGRES_HOST="db" +POSTGRES_DB="planifetsDB" +POSTGRES_PASSWORD="postgres" +POSTGRES_USER="postgres" +POSTGRES_PORT=5433 -LOG_LEVELS="log,error,warn,debug" # Log levels: "log,error,warn,debug,fatal,verbose" \ No newline at end of file +# Logging Levels +LOG_LEVELS="log,error,warn,debug" # Options: log,error,warn,debug,fatal,verbose \ No newline at end of file diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..8fb0015 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,85 @@ +name: CD + +on: + push: + branches: + - main + tags: + - 'v*' + pull_request: # fixme: test purpose only + branches: ['main'] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'yarn' + cache-dependency-path: 'yarn.lock' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run tests + run: yarn run test + + - name: Build project + run: yarn run build + + docker: + runs-on: ubuntu-latest + needs: [build-and-test] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + # if: github.event_name != 'pull_request' + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Login to Registry + # if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true # ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + semantic-release: + runs-on: ubuntu-latest + needs: [docker] + steps: + - name: Semantic Release + uses: cycjimmy/semantic-release-action@v4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa5153b..4c5a80d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,5 @@ -name: ci +name: CI + on: push: branches: [main] @@ -13,7 +14,7 @@ jobs: node-version: [18.x] steps: - - name: Checkout to code + - name: Checkout code uses: actions/checkout@v3 - name: Setup Node.js @@ -26,11 +27,11 @@ jobs: - name: Install dependencies run: yarn install --frozen-lockfile - - name: lint + - name: Lint code run: yarn run lint - name: Run tests - run: yarn test + run: yarn run test - - name: Build + - name: Build project run: yarn run build diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5e3a0c4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# syntax=docker/dockerfile:1 + +# Base dependencies +FROM node:18-alpine AS base +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn install + +# Development +FROM base AS development +WORKDIR /app +COPY . ./ + +RUN yarn install --frozen-lockfile && yarn global add @nestjs/cli && yarn prisma:generate + +ENV NODE_ENV=development +# Required for hot-reloading +ENV CHOKIDAR_USEPOLLING=true + +CMD [ "yarn", "start:dev" ] + +# Build +FROM base AS build +WORKDIR /app +COPY . ./ + +RUN yarn build + +# Production +FROM node:18-alpine AS production +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn install --production --frozen-lockfile + +COPY --from=build /app/dist ./dist +COPY prisma ./prisma +RUN yarn prisma:generate + +ENV NODE_ENV=production + +CMD [ "yarn", "start:prod" ] + +EXPOSE 3000 diff --git a/charts/planifets-backend/Chart.yaml b/charts/planifets-backend/Chart.yaml new file mode 100644 index 0000000..4104841 --- /dev/null +++ b/charts/planifets-backend/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: planifets-backend +description: A Helm chart for Kubernetes +type: application +version: 0.2.0 +appVersion: '0.1.0' diff --git a/charts/planifets-backend/templates/app-deployment.yaml b/charts/planifets-backend/templates/app-deployment.yaml new file mode 100644 index 0000000..bd88a7b --- /dev/null +++ b/charts/planifets-backend/templates/app-deployment.yaml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ .Release.Name }}-{{ .Chart.Name }}" + labels: + app: "{{ .Chart.Name }}" +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: "{{ .Chart.Name }}" + template: + metadata: + labels: + app: "{{ .Chart.Name }}" + spec: + containers: + - name: app + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + ports: + - containerPort: {{ .Values.service.port }} + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: "{{ .Values.postgresSecretName }}" + key: DATABASE_URL + - name: NODE_ENV + value: production + - name: PORT + value: "{{ .Values.service.port }}" + livenessProbe: + httpGet: + path: / + port: {{ .Values.service.port }} + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: / + port: {{ .Values.service.port }} + initialDelaySeconds: 5 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: "{{ .Release.Name }}-{{ .Chart.Name }}-service" +spec: + selector: + app: "{{ .Chart.Name }}" + ports: + - protocol: TCP + port: {{ .Values.service.port }} + targetPort: {{ .Values.service.port }} + type: {{ .Values.service.type }} diff --git a/charts/planifets-backend/templates/ingress.yaml b/charts/planifets-backend/templates/ingress.yaml new file mode 100644 index 0000000..fc2b161 --- /dev/null +++ b/charts/planifets-backend/templates/ingress.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: "{{ .Release.Name }}-{{ .Chart.Name }}-ingress" +spec: + ingressClassName: nginx + rules: + - host: {{ (index .Values.ingress.hosts 0).host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: "{{ .Release.Name }}-{{ .Chart.Name }}-service" + port: + number: {{ .Values.service.port }} diff --git a/charts/planifets-backend/templates/postgres-pvc.yaml b/charts/planifets-backend/templates/postgres-pvc.yaml new file mode 100644 index 0000000..ae36da5 --- /dev/null +++ b/charts/planifets-backend/templates/postgres-pvc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi # todo \ No newline at end of file diff --git a/charts/planifets-backend/templates/postgres-secret.yaml b/charts/planifets-backend/templates/postgres-secret.yaml new file mode 100644 index 0000000..ddf3ef4 --- /dev/null +++ b/charts/planifets-backend/templates/postgres-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.postgresSecretName }} +type: Opaque +stringData: + POSTGRES_USER: {{ .Values.postgres.user }} + POSTGRES_PASSWORD: {{ .Values.postgres.password }} + POSTGRES_DB: {{ .Values.postgres.db }} + DATABASE_URL: "postgresql://{{ .Values.postgres.user }}:{{ .Values.postgres.password }}@postgres:5432/{{ .Values.postgres.db }}?schema=public" diff --git a/charts/planifets-backend/templates/postgres.yaml b/charts/planifets-backend/templates/postgres.yaml new file mode 100644 index 0000000..42dc642 --- /dev/null +++ b/charts/planifets-backend/templates/postgres.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + labels: + app: postgres +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:16 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ .Values.postgresSecretName }} + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.postgresSecretName }} + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: {{ .Values.postgresSecretName }} + key: POSTGRES_DB + volumeMounts: + - name: postgres-storage + mountPath: /var/lib/postgresql/data + livenessProbe: + exec: + command: + - pg_isready + - -U + - {{ .Values.postgres.user }} + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - pg_isready + - -U + - {{ .Values.postgres.user }} + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: postgres-storage + persistentVolumeClaim: + claimName: postgres-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 + protocol: TCP + type: ClusterIP diff --git a/charts/planifets-backend/values.yaml b/charts/planifets-backend/values.yaml new file mode 100644 index 0000000..6484048 --- /dev/null +++ b/charts/planifets-backend/values.yaml @@ -0,0 +1,26 @@ +replicaCount: 1 + +image: + repository: ghcr.io/applets/planifets-backend + pullPolicy: Always + tag: 'pr-47' # todo + +imagePullSecrets: [] +nameOverride: '' +fullnameOverride: '' + +service: + port: 3000 + type: LoadBalancer + +postgres: + user: postgres # todo + password: bEL8BZZU^BF8Rqe9 # todo + db: planifetsDB # todo + +ingress: + enabled: true + hosts: + - host: localhost # fixme (not sure) + +postgresSecretName: postgres-secret diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..e0dfcb7 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,48 @@ +services: + app: + build: + context: . + target: ${TARGET:-development} + ports: + - "${PORT:-3000}:${PORT:-3000}" + environment: + DATABASE_URL: ${DATABASE_URL} + NODE_ENV: development + PORT: ${PORT} + env_file: + - .env + depends_on: + db: + condition: service_healthy + volumes: + - .:/app + - /app/node_modules + networks: + - app-network + restart: unless-stopped + + db: + image: postgres:16 + ports: + - '5433:5432' + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - db-data:/var/lib/postgresql/data + networks: + - app-network + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + db-data: + driver: local + +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..31c5de4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +services: + app: + build: + context: . + target: production + ports: + - "${PORT:-3000}:${PORT:-3000}" + environment: + DATABASE_URL: ${DATABASE_URL} + NODE_ENV: production + PORT: ${PORT:-3000} + depends_on: + db: + condition: service_healthy + networks: + - app-network + restart: unless-stopped + + db: + image: postgres:16 + ports: + - '5433:5432' + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - db-data:/var/lib/postgresql/data + networks: + - app-network + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + db-data: + driver: local + +networks: + app-network: + driver: bridge diff --git a/package.json b/package.json index 1f7054a..09ae7ad 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "author": "", "private": true, - "license": "UNLICENSED", + "license": "Apache-2.0", "scripts": { "build": "nest build", "start": "nest start", @@ -12,6 +12,9 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", + "docker:dev": "docker-compose -f docker-compose.dev.yml up -d", + "docker:build-dev": "docker-compose -f docker-compose.dev.yml up -d --build", + "docker:prod": "docker-compose -f docker-compose.yml up -d --build --force-recreate --remove-orphans", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "lint": "eslint src --ext .ts", "refresh": "rm -rf dist && rm -rf node_modules && yarn cache clean && yarn install", diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 831c33d..bfc2dbb 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -2,8 +2,4 @@ import * as path from 'path'; export default () => ({ pdfOutputPath: path.resolve(__dirname, '../../test/pdf/output'), - redis: { - host: process.env.REDIS_HOST ?? 'localhost', - port: parseInt(process.env.REDIS_PORT ?? '6379', 10), - }, }); diff --git a/src/main.ts b/src/main.ts index 71a98a5..8f26cb3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { HttpExceptionFilter } from './common/exceptions/http-exception.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); + const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; app.useGlobalFilters(new HttpExceptionFilter()); @@ -22,7 +23,7 @@ async function bootstrap() { .setTitle('PlanifÉTS API') .setExternalDoc('JSON API Documentation', '/api-json') .setVersion('1.0') - .addServer('http://localhost:3000/', 'Local environment') + .addServer(`http://localhost:${port}/`, 'Local environment') .build(); const document = SwaggerModule.createDocument(app, swaggerConfig); const swaggerOptions = { @@ -33,7 +34,7 @@ async function bootstrap() { SwaggerModule.setup('api', app, document, swaggerOptions); //Start the app - await app.listen(process.env.PORT ? parseInt(process.env.PORT) : 3000); - console.log(`Swagger is running on http://localhost:${process.env.PORT}/api`); + await app.listen(port); + console.log(`Swagger is running on http://localhost:${port}/api`); } bootstrap();