diff --git a/.github/workflows/containers-publish.yml b/.github/workflows/containers-publish.yml index 9cd5fcce..6ad4bd88 100644 --- a/.github/workflows/containers-publish.yml +++ b/.github/workflows/containers-publish.yml @@ -1,69 +1,73 @@ -name: "Containers: Publish" - -on: - push: - tags: ["v*"] - -permissions: - packages: write - -jobs: - release-containers: - name: Build and Push - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Login to ghcr.io Docker registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Compute Docker container image addresses - run: | - DOCKER_REPOSITORY="ghcr.io/${GITHUB_REPOSITORY,,}" - DOCKER_TAG="${GITHUB_REF:11}" - - echo "DOCKER_REPOSITORY=${DOCKER_REPOSITORY}" >> $GITHUB_ENV - echo "DOCKER_TAG=${DOCKER_TAG}" >> $GITHUB_ENV - - echo "Using: ${DOCKER_REPOSITORY}/*:${DOCKER_TAG}" - - # - name: 'Pull previous Docker container image: :latest' - # run: docker pull "${DOCKER_REPOSITORY}:latest" || true - - - name: "Pull previous Docker container image: frontend-static:latest" - run: docker pull "${DOCKER_REPOSITORY}/frontend-static:latest" || true - - - name: "Build Docker container image: frontend-static:latest" - run: | - docker build \ - --cache-from "${DOCKER_REPOSITORY}/frontend-static:latest" \ - --file frontend/Dockerfile.demo \ - --build-arg SERVER_NAME=localhost \ - --tag "${DOCKER_REPOSITORY}/frontend-static:latest" \ - --tag "${DOCKER_REPOSITORY}/frontend-static:${DOCKER_TAG}" \ - frontend - - name: "Push Docker container image frontend-static:latest" - run: docker push "${DOCKER_REPOSITORY}/frontend-static:latest" - - - name: "Push Docker container image frontend-static:v*" - run: docker push "${DOCKER_REPOSITORY}/frontend-static:${DOCKER_TAG}" -# -# -# - name: 'Build Docker container image: backend:latest' -# run: | -# cd backend && \ -# make && \ -# docker image tag "${DOCKER_REPOSITORY}/backend/local:latest" "${DOCKER_REPOSITORY}/backend:latest" -# -# - name: Push Docker container image backend:latest -# run: docker push "${DOCKER_REPOSITORY}/backend:latest" -# -# - name: Push Docker container image backend:v* -# run: docker push "${DOCKER_REPOSITORY}/backend:${DOCKER_TAG}" - -# - name: Push Docker container image :v*" -# run: docker push "${DOCKER_REPOSITORY}:${DOCKER_TAG}" +name: 'Containers: Publish' + +on: + push: + tags: [ 'v*' ] + +permissions: + packages: write + +jobs: + release-containers: + name: Build and Push + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v3 + + - name: Login to ghcr.io Docker registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute Docker container image addresses + run: | + DOCKER_REPOSITORY="ghcr.io/${GITHUB_REPOSITORY,,}" + DOCKER_TAG="${GITHUB_REF:11}" + + echo "DOCKER_REPOSITORY=${DOCKER_REPOSITORY}" >> $GITHUB_ENV + echo "DOCKER_TAG=${DOCKER_TAG}" >> $GITHUB_ENV + + echo "Using: ${DOCKER_REPOSITORY}/*:${DOCKER_TAG}" + + # - name: 'Pull previous Docker container image: :latest' + # run: docker pull "${DOCKER_REPOSITORY}:latest" || true + - name: 'Pull previous Docker container image: backend:latest' + run: docker pull "${DOCKER_REPOSITORY}/backend:latest" || true + + - name: 'Build Docker container image: backend:latest' + run: | + cd server + docker build \ + --cache-from "${DOCKER_REPOSITORY}/backend:latest" \ + --file Dockerfile.prod \ + --tag "${DOCKER_REPOSITORY}/backend:latest" \ + --tag "${DOCKER_REPOSITORY}/backend:${DOCKER_TAG}" \ + . + cd .. + - name: 'Push Docker container image backend:latest' + run: docker push "${DOCKER_REPOSITORY}/backend:latest" + + - name: 'Push Docker container image backend:v*' + run: docker push "${DOCKER_REPOSITORY}/backend:${DOCKER_TAG}" + + - name: 'Pull previous Docker container image: frontend:latest' + run: docker pull "${DOCKER_REPOSITORY}/frontend:latest" || true + + - name: 'Build Docker container image: frontend:latest' + run: | + cd ./frontend + docker build \ + --cache-from "${DOCKER_REPOSITORY}/frontend:latest" \ + --file Dockerfile.prod \ + --tag "${DOCKER_REPOSITORY}/frontend:latest" \ + --tag "${DOCKER_REPOSITORY}/frontend:${DOCKER_TAG}" \ + . + cd .. + - name: 'Push Docker container image frontend:latest' + run: docker push "${DOCKER_REPOSITORY}/frontend:latest" + + - name: 'Push Docker container image frontend:v*' + run: docker push "${DOCKER_REPOSITORY}/frontend:${DOCKER_TAG}" diff --git a/.holo/branches/helm-chart/_balancer.toml b/.holo/branches/helm-chart/_balancer.toml new file mode 100644 index 00000000..3acdeaee --- /dev/null +++ b/.holo/branches/helm-chart/_balancer.toml @@ -0,0 +1,3 @@ +[holomapping] +root = "helm-chart" +files = "**" \ No newline at end of file diff --git a/.holo/config.toml b/.holo/config.toml new file mode 100644 index 00000000..aa3dacf3 --- /dev/null +++ b/.holo/config.toml @@ -0,0 +1,2 @@ +[holospace] +name = "balancer" diff --git a/config/env/.env.prod.db b/config/env/.env.prod.db deleted file mode 100644 index 3c73c247..00000000 --- a/config/env/.env.prod.db +++ /dev/null @@ -1,3 +0,0 @@ -POSTGRES_USER=set_me -POSTGRES_PASSWORD=set_me -POSTGRES_DB=balancer_prod \ No newline at end of file diff --git a/config/env/env.dev b/config/env/env.dev index 92f51aee..f05b5552 100644 --- a/config/env/env.dev +++ b/config/env/env.dev @@ -1,4 +1,4 @@ -DEBUG=True +DEBUG=TRUE SECRET_KEY=foo DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] SQL_ENGINE=django.db.backends.postgresql @@ -12,4 +12,5 @@ LOGIN_REDIRECT_URL= OPENAI_API_KEY= PINECONE_API_KEY= EMAIL_HOST_USER= -EMAIL_HOST_PASSWORD= \ No newline at end of file +EMAIL_HOST_PASSWORD= +SUPER_USER_PASSWORD=adminpassword diff --git a/config/env/env.prod b/config/env/env.prod deleted file mode 100644 index 12b3491a..00000000 --- a/config/env/env.prod +++ /dev/null @@ -1,14 +0,0 @@ -DEBUG=0 -SECRET_KEY=change_this -DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] -DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] -SQL_ENGINE=django.db.backends.postgresql -SQL_DATABASE=balancer_prod -SQL_USER=set_me -SQL_PASSWORD=set_me -SQL_HOST=db -SQL_PORT=5432 -DATABASE=postgres -LOGIN_REDIRECT_URL= -EMAIL_HOST_USER= -EMAIL_HOST_PASSWORD= \ No newline at end of file diff --git a/config/env/env.prod.db b/config/env/env.prod.db deleted file mode 100644 index 3c73c247..00000000 --- a/config/env/env.prod.db +++ /dev/null @@ -1,3 +0,0 @@ -POSTGRES_USER=set_me -POSTGRES_PASSWORD=set_me -POSTGRES_DB=balancer_prod \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 2113abeb..00000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,43 +0,0 @@ -version: '3.8' -services: - db: - image: postgres:15 - volumes: - - postgres_data:/var/lib/postgresql/data/ - environment: - - POSTGRES_USER=balancer - - POSTGRES_PASSWORD=balancer - - POSTGRES_DB=balancer_dev - ports: - - "5432:5432" - backend: - image: balancer-backend - build: - context: server - dockerfile: Dockerfile.prod - ports: - - "8000:8000" - env_file: - - ./config/env/env.dev - depends_on: - - db - frontend: - image: balancer-frontend - build: - context: frontend - dockerfile: Dockerfile - args: - - IMAGE_NAME=balancer-frontend - ports: - - "3000:3000" - environment: - - CHOKIDAR_USEPOLLING=true - # - VITE_API_BASE_URL=https://balancertestsite.com/ - volumes: - - "./frontend:/usr/src/app:delegated" - - "/usr/src/app/node_modules/" - depends_on: - - backend - -volumes: - postgres_data: \ No newline at end of file diff --git a/docker-compose.prodBackup.yml b/docker-compose.prodBackup.yml deleted file mode 100644 index a7f0ae12..00000000 --- a/docker-compose.prodBackup.yml +++ /dev/null @@ -1,30 +0,0 @@ -version: '3.8' -services: - db: - image: postgres:15 - volumes: - - postgres_data:/var/lib/postgresql/data/ - env_file: - - ./config/env/.env.prod.db - backend: - build: - context: ./server - dockerfile: Dockerfile.prod - command: gunicorn balancer_backend.wsgi:application --bind 0.0.0.0:8000 - ports: - - 8000:8000 - env_file: - - ./config/env/.env.prod - depends_on: - - db - frontend: - build: - context: ./frontend - dockerfile: Dockerfile.prod - ports: - - "3000:80" - depends_on: - - backend - -volumes: - postgres_data: \ No newline at end of file diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod index c0648913..f091d77f 100644 --- a/frontend/Dockerfile.prod +++ b/frontend/Dockerfile.prod @@ -10,10 +10,16 @@ COPY . . RUN npm run build +FROM ghcr.io/codeforphilly/balancer-main/backend:latest as backend +# The frontend needs to get the static files from the backend for the django admin site and +# django rest framework error pages. Pulling them from the built image works but may not be the best way to do this +# because it causes the frontend image to depend on the backend image. + FROM nginx:latest -COPY nginx.conf /etc/nginx/conf.d/default.conf +# The nginx.conf file will be replaced from the helm chart. -COPY --from=builder /usr/src/app/build /usr/share/nginx/html +COPY --from=builder /usr/src/server/build /usr/share/nginx/html +COPY --from=backend /usr/src/app/static /usr/share/nginx/html/static -EXPOSE 80 \ No newline at end of file +EXPOSE 80 diff --git a/frontend/nginx.conf b/frontend/nginx.conf deleted file mode 100644 index 05d512d6..00000000 --- a/frontend/nginx.conf +++ /dev/null @@ -1,10 +0,0 @@ -server { - listen 80; - server_name backend; - - location / { - root /usr/share/nginx/html; - index index.html; - try_files $uri $uri/ /index.html; - } -} \ No newline at end of file diff --git a/frontend/src/components/Header/Chat.tsx b/frontend/src/components/Header/Chat.tsx index b64db23f..c532aac7 100644 --- a/frontend/src/components/Header/Chat.tsx +++ b/frontend/src/components/Header/Chat.tsx @@ -83,7 +83,8 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { }; const sendMessage = (message: ChatLogItem[]) => { - const baseUrl = import.meta.env.VITE_API_BASE_URL; + let baseUrl = window.location.origin + '/api'; + baseUrl = baseUrl.replace(":3000", ":8000"); const url = `${baseUrl}/chatgpt/chat`; const apiMessages = message.map((messageObject) => { diff --git a/frontend/src/pages/DrugSummary/DrugSummaryForm.tsx b/frontend/src/pages/DrugSummary/DrugSummaryForm.tsx index 61b80ac2..446a991d 100644 --- a/frontend/src/pages/DrugSummary/DrugSummaryForm.tsx +++ b/frontend/src/pages/DrugSummary/DrugSummaryForm.tsx @@ -108,7 +108,8 @@ const DrugSummaryForm = () => { }; const sendMessage = (message: ChatLogItem[]) => { - const baseUrl = import.meta.env.VITE_API_BASE_URL; + let baseUrl = window.location.origin + '/api'; + baseUrl = baseUrl.replace(":3000", ":8000"); const url = `${baseUrl}/chatgpt/chat`; const apiMessages = message.map((messageObject) => { diff --git a/frontend/src/pages/DrugSummary/DrugSummaryFormBackup.tsx b/frontend/src/pages/DrugSummary/DrugSummaryFormBackup.tsx index 7a1f2e40..b9b94511 100644 --- a/frontend/src/pages/DrugSummary/DrugSummaryFormBackup.tsx +++ b/frontend/src/pages/DrugSummary/DrugSummaryFormBackup.tsx @@ -45,7 +45,8 @@ const DrugSummaryForm = () => { } const contentType = url ? "application/json" : "multi-part/form"; - const baseUrl = import.meta.env.VITE_API_BASE_URL; + let baseUrl = window.location.origin + '/api'; + baseUrl = baseUrl.replace(":3000", ":8000"); const completeBaseURL = `${baseUrl}/chatgpt`; try { // TODO change this to actual endpoint url once hosted diff --git a/frontend/src/pages/Feedback/FeedbackForm.tsx b/frontend/src/pages/Feedback/FeedbackForm.tsx index 68a3b2a1..5e5ce819 100644 --- a/frontend/src/pages/Feedback/FeedbackForm.tsx +++ b/frontend/src/pages/Feedback/FeedbackForm.tsx @@ -64,6 +64,7 @@ const FeedbackForm = () => { try { const res = await axios.post( + // this URL won't work "http://localhost:3001/text_extraction", formData, { @@ -86,14 +87,16 @@ const FeedbackForm = () => { }, onSubmit: async (values) => { setFeedback(""); + let baseUrl = window.location.origin + '/api'; + baseUrl = baseUrl.replace(":3000", ":8000"); try { // Call 1: Create Feedback request - const response = await axios.post( - "http://localhost:8000/api/jira/create_new_feedback/", + const response = await axios.post(baseUrl + "/jira/create_new_feedback/", { name: values.name, email: values.email, message: values.message, + // the backend endpoint also expects a feedbackType }, { headers: { @@ -113,7 +116,7 @@ const FeedbackForm = () => { formData.append("attachment", values.image); const response2 = await axios.post( - "http://localhost:8000/api/jira/upload_servicedesk_attachment/", + baseUrl + "/jira/upload_servicedesk_attachment/", formData, { headers: { @@ -128,7 +131,7 @@ const FeedbackForm = () => { // Step 3: Attach upload image to feedback request const response3 = await axios.post( - "http://localhost:8000/api/jira/attach_feedback_attachment/", + baseUrl + "/jira/attach_feedback_attachment/", { issueKey: issueKey, tempAttachmentId: attachmentId, diff --git a/frontend/src/pages/PatientManager/NewPatientForm.tsx b/frontend/src/pages/PatientManager/NewPatientForm.tsx index 19b780cc..5a55f86c 100644 --- a/frontend/src/pages/PatientManager/NewPatientForm.tsx +++ b/frontend/src/pages/PatientManager/NewPatientForm.tsx @@ -132,7 +132,8 @@ const NewPatientForm = ({ setIsLoading(true); // Start loading try { - const baseUrl = import.meta.env.VITE_API_BASE_URL; + let baseUrl = window.location.origin + '/api'; + baseUrl = baseUrl.replace(":3000", ":8000"); const url = `${baseUrl}/chatgpt`; console.log(payload); diff --git a/frontend/src/pages/PatientManager/PatientSummary.tsx b/frontend/src/pages/PatientManager/PatientSummary.tsx index f4594b6b..33c8ec11 100644 --- a/frontend/src/pages/PatientManager/PatientSummary.tsx +++ b/frontend/src/pages/PatientManager/PatientSummary.tsx @@ -59,7 +59,8 @@ const PatientSummary = ({ setClickedMedication(medication); setLoading(true); try { - const baseUrl = import.meta.env.VITE_API_BASE_URL; + let baseUrl = window.location.origin + '/api'; + baseUrl = baseUrl.replace(":3000", ":8000"); const postBody = { diagnosis: medication, }; diff --git a/frontend/src/services/actions/auth.tsx b/frontend/src/services/actions/auth.tsx index 3a29bc38..de74fac1 100644 --- a/frontend/src/services/actions/auth.tsx +++ b/frontend/src/services/actions/auth.tsx @@ -75,7 +75,8 @@ export const checkAuthenticated = () => async (dispatch: AppDispatch) => { }; const body = JSON.stringify({ token: localStorage.getItem("access") }); - const baseUrl = import.meta.env.VITE_API_BASE_URL; + let baseUrl = window.location.origin; + baseUrl = baseUrl.replace(":3000", ":8000"); const url = `${baseUrl}/auth/jwt/verify/`; try { const res = await axios.post(url, body, config); @@ -112,7 +113,8 @@ export const load_user = (): ThunkType => async (dispatch: AppDispatch) => { Accept: "application/json", }, }; - const baseUrl = import.meta.env.VITE_API_BASE_URL; + let baseUrl = window.location.origin; + baseUrl = baseUrl.replace(":3000", ":8000"); const url = `${baseUrl}/auth/users/me/`; try { const res = await axios.get(url, config); @@ -143,7 +145,8 @@ export const login = }; const body = JSON.stringify({ email, password }); - const baseUrl = import.meta.env.VITE_API_BASE_URL; + let baseUrl = window.location.origin; + baseUrl = baseUrl.replace(":3000", ":8000"); const url = `${baseUrl}/auth/jwt/create/`; try { const res = await axios.post(url, body, config); @@ -184,7 +187,8 @@ export const reset_password = }; console.log("yes"); const body = JSON.stringify({ email }); - const baseUrl = import.meta.env.VITE_API_BASE_URL; + let baseUrl = window.location.origin; + baseUrl = baseUrl.replace(":3000", ":8000"); const url = `${baseUrl}/auth/users/reset_password/`; try { await axios.post(url, body, config); @@ -214,7 +218,8 @@ export const reset_password_confirm = }; const body = JSON.stringify({ uid, token, new_password, re_new_password }); - const baseUrl = import.meta.env.VITE_API_BASE_URL; + let baseUrl = window.location.origin; + baseUrl = baseUrl.replace(":3000", ":8000"); const url = `${baseUrl}/auth/users/reset_password_confirm/`; try { const response = await axios.post(url, body, config); diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml index b4da3f41..b22e9e9e 100644 --- a/helm-chart/Chart.yaml +++ b/helm-chart/Chart.yaml @@ -1,9 +1,9 @@ -name: nginx-helm-chart -description: A generated Helm Chart for nginx-helm-chart from Skippbox Kompose -version: 0.0.2 +name: helm-chart +description: A Helm Chart for the Balancer project +version: 0.0.1 apiVersion: v2 keywords: - - nginx-helm-chart + - helm-chart sources: - https://github.com/CodeForPhilly/balancer-main home: https://opencollective.com/code-for-philly/projects/balancer diff --git a/helm-chart/README.md b/helm-chart/README.md index e69de29b..d4b20b79 100644 --- a/helm-chart/README.md +++ b/helm-chart/README.md @@ -0,0 +1,14 @@ +This chart was initially created by Kompose + +To seal secrets: +--- + + `> export SEALED_SECRETS_CERT=https://sealed-secrets.sandbox.k8s.phl.io/v1/cert.pem` + + `> kubeseal -f my-secret.yaml -o yaml -w my-sealed-secret.yaml` + + +Relevant file locations on GitHub: +--- +- Release values specific to the cfp-sandbox-cluster: https://github.com/CodeForPhilly/cfp-sandbox-cluster/blob/main/balancer/release-values.yaml +- Sealed secrets: https://github.com/CodeForPhilly/cfp-sandbox-cluster/blob/main/balancer.secrets/ diff --git a/helm-chart/templates/.gitignore b/helm-chart/templates/.gitignore new file mode 100644 index 00000000..50e29c41 --- /dev/null +++ b/helm-chart/templates/.gitignore @@ -0,0 +1 @@ +*secret.yaml diff --git a/helm-chart/templates/backend-deployment.yaml b/helm-chart/templates/backend-deployment.yaml new file mode 100644 index 00000000..884ed5e3 --- /dev/null +++ b/helm-chart/templates/backend-deployment.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: backend + name: backend +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: backend + strategy: {} + template: + metadata: + labels: + app.kubernetes.io/component: backend + spec: + containers: + - args: + - gunicorn + - balancer_backend.wsgi:application + - --bind + - 0.0.0.0:8000 + envFrom: + - configMapRef: + name: backend-env + - secretRef: + name: {{ .Values.backend.existingSecret }} + env: + - name: SQL_HOST + value: db-service + - name: SQL_USER + valueFrom: + secretKeyRef: + key: POSTGRES_USER + name: postgresql + - name: SQL_DATABASE + valueFrom: + secretKeyRef: + key: POSTGRES_DB + name: postgresql + - name: SQL_PASSWORD + valueFrom: + secretKeyRef: + key: POSTGRES_PASSWORD + name: postgresql + image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}" + imagePullPolicy: Always + name: backend + ports: + - containerPort: 8000 + protocol: TCP + resources: {} + restartPolicy: Always diff --git a/helm-chart/templates/backend-env-configmap.yaml b/helm-chart/templates/backend-env-configmap.yaml new file mode 100644 index 00000000..4cb2c0d9 --- /dev/null +++ b/helm-chart/templates/backend-env-configmap.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +data: + {{- toYaml .Values.backend.env | nindent 2 }} + SQL_PORT: "{{ .Values.postgresql.port }}" + DJANGO_ALLOWED_HOSTS: | + {{- $hosts := .Values.ingress.hosts -}} + {{- $hostsList := "localhost frontend 127.0.0.1 [::1]" -}} + {{- if .Values.ingress.enabled }} + {{- range $index, $host := $hosts -}} + {{- $hostsList = printf "%s %s" $hostsList $host.host -}} + {{- end -}} + {{- end }} + {{ $hostsList }} +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: backend + name: backend-env diff --git a/helm-chart/templates/backend-service.yaml b/helm-chart/templates/backend-service.yaml new file mode 100644 index 00000000..b15cdd8c --- /dev/null +++ b/helm-chart/templates/backend-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: backend + name: backend +spec: + ports: + - name: "gunicorn" + port: 8000 + targetPort: 8000 + selector: + app.kubernetes.io/component: backend diff --git a/helm-chart/templates/db-service.yaml b/helm-chart/templates/db-service.yaml new file mode 100644 index 00000000..843f11c0 --- /dev/null +++ b/helm-chart/templates/db-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: db-service +spec: + selector: + app.kubernetes.io/component: db + ports: + - protocol: TCP + port: {{ .Values.postgresql.port }} + type: NodePort + \ No newline at end of file diff --git a/helm-chart/templates/db-statefulset.yaml b/helm-chart/templates/db-statefulset.yaml new file mode 100644 index 00000000..1d0ced2b --- /dev/null +++ b/helm-chart/templates/db-statefulset.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app.kubernetes.io/component: db + name: db +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: db + template: + metadata: + labels: + io.kompose.network/balancer-main-default: "true" + app.kubernetes.io/component: db + spec: + containers: + {{- if .Values.postgresql.existingSecret }} + - envFrom: + - secretRef: + name: {{ .Values.postgresql.existingSecret }} + {{- end }} + image: "{{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }}" + name: db + resources: {} + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: postgres-data + subPath: data + + restartPolicy: Always + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: postgres-data-claim + serviceName: "db" diff --git a/helm-chart/templates/frontend-deployment.yaml b/helm-chart/templates/frontend-deployment.yaml new file mode 100644 index 00000000..11a8f702 --- /dev/null +++ b/helm-chart/templates/frontend-deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: frontend + name: frontend +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: frontend + strategy: {} + template: + metadata: + annotations: + kompose.cmd: kompose convert -c -f docker-compose.prod.yml -o helm-chart + kompose.version: 1.31.2 (a92241f79) + labels: + io.kompose.network/balancer-main-default: "true" + app.kubernetes.io/component: frontend + spec: + containers: + - image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}" + imagePullPolicy: Always + name: frontend + ports: + - containerPort: 80 + protocol: TCP + volumeMounts: + - mountPath: /etc/nginx/nginx.conf + name: nginx-conf + subPath: nginx.conf + readOnly: true + resources: {} + volumes: + - name: nginx-conf + configMap: + name: nginx-conf + items: + - key: nginx.conf + path: nginx.conf + restartPolicy: Always diff --git a/helm-chart/templates/frontend-service.yaml b/helm-chart/templates/frontend-service.yaml new file mode 100644 index 00000000..5548cf12 --- /dev/null +++ b/helm-chart/templates/frontend-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert -c -f docker-compose.prod.yml -o helm-chart + kompose.version: 1.31.2 (a92241f79) + labels: + app.kubernetes.io/component: frontend + name: frontend +spec: + ports: + - name: "http" + port: 80 + selector: + app.kubernetes.io/component: frontend diff --git a/helm-chart/templates/frontend-static-deployment.yaml b/helm-chart/templates/frontend-static-deployment.yaml deleted file mode 100644 index ae4b72f8..00000000 --- a/helm-chart/templates/frontend-static-deployment.yaml +++ /dev/null @@ -1,54 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert -c -f nginx-docker-compose.yml -o nginx-helm-chart - kompose.version: 1.31.2 (a92241f79) - creationTimestamp: null - labels: - io.kompose.service: frontend-static - name: frontend-static -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: frontend-static - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose convert -c -f nginx-docker-compose.yml -o nginx-helm-chart - kompose.version: 1.31.2 (a92241f79) - creationTimestamp: null - labels: - io.kompose.network/frontend-default: "true" - io.kompose.service: frontend-static - spec: - containers: - - env: - - name: CHOKIDAR_USEPOLLING - value: "true" - - name: VITE_API_BASE_URL - value: { { .Values.VITE_API_BASE_URL } } - - image: ghcr.io/codeforphilly/balancer-main/frontend-static:latest - name: frontend-static - ports: - - containerPort: 80 - protocol: TCP - volumeMounts: - - mountPath: /etc/nginx/nginx.conf - name: nginx-conf - subPath: nginx.conf - readOnly: true - resources: {} - volumes: - - name: nginx-conf - configMap: - name: nginx-conf - items: - - key: nginx.conf - path: nginx.conf - restartPolicy: Always -status: {} diff --git a/helm-chart/templates/frontend-static-service.yaml b/helm-chart/templates/frontend-static-service.yaml deleted file mode 100644 index 26c3db19..00000000 --- a/helm-chart/templates/frontend-static-service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert -c -f nginx-docker-compose.yml -o nginx-helm-chart - kompose.version: 1.31.2 (a92241f79) - creationTimestamp: null - labels: - io.kompose.service: frontend-static - name: frontend-static -spec: - ports: - - name: "http" - port: 80 - protocol: TCP - selector: - io.kompose.service: frontend-static -status: - loadBalancer: {} diff --git a/helm-chart/templates/ingress-no-proxy.yaml b/helm-chart/templates/ingress-no-proxy.yaml new file mode 100644 index 00000000..e09fb657 --- /dev/null +++ b/helm-chart/templates/ingress-no-proxy.yaml @@ -0,0 +1,68 @@ +{{/* This is an attempt at an ingress controller that would send traffic to both frontend and backend containers, +instead of having the frontend container act as a proxy for backend. I couldn't get it working though. */}} + +{{- if false -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: balancer-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . | trimSuffix "/" }}/api/ + pathType: Prefix + backend: + service: + name: backend + port: + name: gunicorn + - path: {{ . | trimSuffix "/" }}/admin/ + pathType: Prefix + backend: + service: + name: backend + port: + name: gunicorn + - path: {{ . | trimSuffix "/" }}/admin + pathType: Exact + backend: + service: + name: backend + port: + name: gunicorn + - path: {{ . | trimSuffix "/" }}/auth/ + pathType: Prefix + backend: + service: + name: backend + port: + name: gunicorn + - path: {{ . }} + pathType: Prefix + backend: + service: + name: frontend + port: + name: http + {{- end }} + {{- end }} +{{- end }} diff --git a/helm-chart/templates/ingress.yaml b/helm-chart/templates/ingress.yaml new file mode 100644 index 00000000..08442865 --- /dev/null +++ b/helm-chart/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: balancer-ingress + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + pathType: Prefix + backend: + service: + name: frontend + port: + name: http + {{- end }} + {{- end }} +{{- end }} diff --git a/helm-chart/templates/nginx-configmap.yaml b/helm-chart/templates/nginx-configmap.yaml index ecbf1e6b..866d8491 100644 --- a/helm-chart/templates/nginx-configmap.yaml +++ b/helm-chart/templates/nginx-configmap.yaml @@ -1,44 +1,76 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: nginx-conf -# https://stackoverflow.com/questions/64178370/custom-nginx-conf-from-configmap-in-kubernetes -data: - nginx.conf: | - user nginx; - worker_processes 1; - events { - worker_connections 1024; - } - http { - include /etc/nginx/mime.types; - error_log /var/log/nginx/error_log; - access_log /var/log/nginx/access_log; - server { - listen 80; - listen [::]:80; - server_name {{ .Values.nginx.serverName }}; - - location /access_log { - alias /var/log/nginx/access_log; - } - location /error_log { - alias /var/log/nginx/error_log; - } - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - } - - #error_page 404 /404.html; - - # redirect server error pages to the static page /50x.html - # - #error_page 500 502 503 504 /50x.html; - #location = /50x.html { - # root /usr/share/nginx/html; - - #} - } - } +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: frontend + name: nginx-conf + +data: + nginx.conf: | + worker_processes 1; + + user nobody nogroup; + # 'user nobody nobody;' for systems with 'nobody' as a group instead + error_log /var/log/nginx/error.log warn; + pid /var/run/nginx.pid; + + events { + worker_connections 1024; # increase if you have lots of clients + accept_mutex off; # set to 'on' if nginx worker_processes > 1 + # 'use epoll;' to enable for Linux 2.6+ + # 'use kqueue;' to enable for FreeBSD, OSX + } + + http { + include mime.types; + # fallback in case we can't determine a type + default_type application/octet-stream; + access_log /var/log/nginx/access.log combined; + sendfile on; + + upstream gunicorn_server { + # fail_timeout=0 means we always retry an upstream even if it failed + # to return a good HTTP response + + # for UNIX domain socket setups + # server unix:/tmp/gunicorn.sock fail_timeout=0; + + # for a TCP configuration + # "backend" here is the name of the kubernetes service + server backend:8000 fail_timeout=0; + } + + server { + # use 'listen 80 deferred;' for Linux + # use 'listen 80 accept_filter=httpready;' for FreeBSD + listen 80 deferred; + client_max_body_size 4G; + + # set the correct host(s) for your site + server_name {{.Values.frontend.server_name}}; + + keepalive_timeout 5; + + # path for static files + root /usr/share/nginx/html; + location ~ (^\/api\/|^\/auth\/|^\/admin) { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + # we don't want nginx trying to do something clever with + # redirects, we set the Host: header above already. + proxy_redirect off; + proxy_pass http://gunicorn_server; + } + + location / { + index index.html; + try_files $uri $uri/ /index.html; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } + } diff --git a/helm-chart/templates/postgres-data-volumeclaim.yaml b/helm-chart/templates/postgres-data-volumeclaim.yaml new file mode 100644 index 00000000..5eb06a7f --- /dev/null +++ b/helm-chart/templates/postgres-data-volumeclaim.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app.kubernetes.io/component: db + name: postgres-data-claim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index 0aed6b2e..89731104 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -1,18 +1,45 @@ -nginx: - serverName: "localhost" - -VITE_API_BASE_URL: https://devnull-as-a-service.com/dev/null - -ingress: - enabled: false - className: "" - annotations: - {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific - tls: [] +# Default values for Balancer-main + +backend: +# Gunicorn/Django container variables + existingSecret: backend + image: + repository: "ghcr.io/codeforphilly/balancer-main/backend" + tag: "latest" + env: + DATABASE: "postgres" + DEBUG: "TRUE" +# DJANGO_ALLOWED_HOSTS: "frontend localhost 127.0.0.1 [::1]" + SQL_ENGINE: "django.db.backends.postgresql" + LOGIN_REDIRECT_URL: "https://balancertestsite.com/login" + EMAIL_HOST: "smtp.gmail.com" + EMAIL_PORT: "587" + EMAIL_HOST_USER: "balancer-noreply@gmail.com" + +frontend: +# Nginx/frontend container variables + image: + repository: "ghcr.io/codeforphilly/balancer-main/frontend" + tag: "latest" + server_name: "localhost" + +postgresql: +# postgres database container variables + existingSecret: postgresql + image: + repository: "postgres" + tag: "15" + port: "5432" + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] diff --git a/server/Dockerfile.prod b/server/Dockerfile.prod index 6555ed6f..aaa22e9a 100644 --- a/server/Dockerfile.prod +++ b/server/Dockerfile.prod @@ -20,6 +20,9 @@ RUN pip install -r requirements.txt # copy project COPY . /usr/src/app +# copy static files to static dir so that the frontend container can copy them +RUN python manage.py collectstatic + # Correct line endings in entrypoint.sh and make it executable RUN sed -i 's/\r$//' entrypoint.sh && chmod +x entrypoint.sh @@ -27,4 +30,4 @@ RUN sed -i 's/\r$//' entrypoint.sh && chmod +x entrypoint.sh ENTRYPOINT ["./entrypoint.prod.sh"] # Default command to run on container start -CMD ["python", "manage.py", "runserver", "0.0.0.0:8000", "--noreload"] +CMD ["gunicorn", "balancer_backend.wsgi:application", "--bind", "0.0.0.0:8000"] diff --git a/server/api/management/commands/createsu.py b/server/api/management/commands/createsu.py index ba8d42c4..3ade4e43 100644 --- a/server/api/management/commands/createsu.py +++ b/server/api/management/commands/createsu.py @@ -1,3 +1,5 @@ +import os + from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model @@ -8,8 +10,9 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): User = get_user_model() if not User.objects.filter(email='admin@example.com').exists(): + password = os.environ.get("SUPER_USER_PASSWORD", "adminpassword") User.objects.create_superuser( - email='admin@example.com', password='adminpassword') + email='admin@example.com', password=password) self.stdout.write(self.style.SUCCESS( 'Successfully created a new superuser')) else: diff --git a/server/api/views/chatgpt/urls.py b/server/api/views/chatgpt/urls.py index 8aba0c7b..4a01d8a3 100644 --- a/server/api/views/chatgpt/urls.py +++ b/server/api/views/chatgpt/urls.py @@ -2,7 +2,7 @@ from api.views.chatgpt import views urlpatterns = [ - path("chatgpt/extract_text/", views.extract_text, name="post_web_text"), - path("chatgpt/diagnosis/", views.diagnosis, name="post_diagnosis"), - path("chatgpt/chat", views.chatgpt, name="chatgpt"), + path("api/chatgpt/extract_text/", views.extract_text, name="post_web_text"), + path("api/chatgpt/diagnosis/", views.diagnosis, name="post_diagnosis"), + path("api/chatgpt/chat", views.chatgpt, name="chatgpt"), ] diff --git a/server/api/views/jira/urls.py b/server/api/views/jira/urls.py index 068b50e2..0d9d004f 100644 --- a/server/api/views/jira/urls.py +++ b/server/api/views/jira/urls.py @@ -2,7 +2,7 @@ from api.views.jira import views urlpatterns = [ - path("jira/create_new_feedback/", views.create_new_feedback, name="create_new_feedback"), - path("jira/upload_servicedesk_attachment", views.upload_servicedesk_attachment, name="upload_servicedesk_attachment"), - path("jira/attach_feedback_attachment", views.upload_servicedesk_attachment, name="attach_feedback_attachment") + path("api/jira/create_new_feedback/", views.create_new_feedback, name="create_new_feedback"), + path("api/jira/upload_servicedesk_attachment", views.upload_servicedesk_attachment, name="upload_servicedesk_attachment"), + path("api/jira/attach_feedback_attachment", views.upload_servicedesk_attachment, name="attach_feedback_attachment") ] diff --git a/server/api/views/listDrugs/urls.py b/server/api/views/listDrugs/urls.py index 593c276b..3bd3fae8 100644 --- a/server/api/views/listDrugs/urls.py +++ b/server/api/views/listDrugs/urls.py @@ -2,5 +2,5 @@ from api.views.listDrugs import views urlpatterns = [ - path("chatgpt/list_drugs", views.medication, name="listDrugs") + path("api/chatgpt/list_drugs", views.medication, name="listDrugs") ] diff --git a/server/api/views/listMeds/urls.py b/server/api/views/listMeds/urls.py index d62dc458..e373da03 100644 --- a/server/api/views/listMeds/urls.py +++ b/server/api/views/listMeds/urls.py @@ -2,5 +2,5 @@ from api.views.listMeds import views urlpatterns = [ - path("chatgpt/list_meds", views.get_medication, name="listMeds") + path("api/chatgpt/list_meds", views.get_medication, name="listMeds") ] diff --git a/server/api/views/risk/urls.py b/server/api/views/risk/urls.py index 30e53424..9250e89d 100644 --- a/server/api/views/risk/urls.py +++ b/server/api/views/risk/urls.py @@ -2,5 +2,5 @@ from api.views.risk import views urlpatterns = [ - path("chatgpt/risk", views.medication, name="risk") + path("api/chatgpt/risk", views.medication, name="risk") ] diff --git a/server/api/views/uploadFile/urls.py b/server/api/views/uploadFile/urls.py index e36dd8c6..8733ec20 100644 --- a/server/api/views/uploadFile/urls.py +++ b/server/api/views/uploadFile/urls.py @@ -2,5 +2,5 @@ from api.views.uploadFile import views urlpatterns = [ - path("chatgpt/uploadFile", views.uploadFiles, name="uploadFiles") + path("api/chatgpt/uploadFile", views.uploadFiles, name="uploadFile") ] diff --git a/server/balancer_backend/settings.py b/server/balancer_backend/settings.py index 13988358..54fec54e 100644 --- a/server/balancer_backend/settings.py +++ b/server/balancer_backend/settings.py @@ -25,7 +25,7 @@ SECRET_KEY = os.environ.get("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = bool(os.environ.get("DEBUG", default=0)) +DEBUG = os.environ.get("DEBUG", '') == "TRUE" # Fetching the value from the environment and splitting to list if necessary. # Fallback to '*' if the environment variable is not set. @@ -36,6 +36,7 @@ if ALLOWED_HOSTS == ['*'] or ALLOWED_HOSTS == ['']: ALLOWED_HOSTS = ['*'] +CSRF_TRUSTED_ORIGINS = map(lambda s: "https://" + s, ALLOWED_HOSTS) # Application definition @@ -111,8 +112,8 @@ } EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = 'smtp.gmail.com' -EMAIL_PORT = 587 +EMAIL_HOST = os.environ.get("EMAIL_HOST", 'smtp.gmail.com') +EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587)) EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") EMAIL_USE_TLS = True @@ -150,11 +151,12 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ +# In production, the frontend copies the collected static files and then serves them at /static -STATIC_URL = '/static/' -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'build/static'), -] +STATIC_URL = 'static/' +# STATICFILES_DIRS = [ +# os.path.join(BASE_DIR, '/static'), +# ] STATIC_ROOT = os.path.join(BASE_DIR, 'static') REST_FRAMEWORK = { @@ -190,7 +192,6 @@ 'ACTIVATION_URL': 'activate/{uid}/{token}', 'SEND_ACTIVATION_EMAIL': True, 'SOCIAL_AUTH_TOKEN_STRATEGY': 'djoser.social.token.jwt.TokenStrategy', - 'SOCIAL_AUTH_ALLOWED_REDIRECT_URIS': ['http://localhost:8000/google', 'http://localhost:8000/facebook'], 'SERIALIZERS': { 'user_create': 'api.models.serializers.UserCreateSerializer', 'user': 'api.models.serializers.UserCreateSerializer', diff --git a/server/balancer_backend/urls.py b/server/balancer_backend/urls.py index 7bec66dc..73cdc68f 100644 --- a/server/balancer_backend/urls.py +++ b/server/balancer_backend/urls.py @@ -26,8 +26,3 @@ url_module = importlib.import_module(f'api.views.{url}.urls') # Append the URL patterns from each imported module urlpatterns += getattr(url_module, 'urlpatterns', []) - -# Add a catch-all URL pattern for handling SPA (Single Page Application) routing -# Serve 'index.html' for any unmatched URL -urlpatterns += [ - re_path(r'^.*$', TemplateView.as_view(template_name='index.html')),] diff --git a/server/entrypoint.prod.sh b/server/entrypoint.prod.sh index 9506422f..e6821ad7 100755 --- a/server/entrypoint.prod.sh +++ b/server/entrypoint.prod.sh @@ -1,5 +1,8 @@ #!/bin/sh +trap "echo Received SIGTERM, exiting...; exit 0" TERM +# To terminate a pod, kubernetes sends a SIGTERM and waits for a period of time for the pod to stop. +# We exit this loop if that happens. if [ "$DATABASE" = "postgres" ] then echo "Waiting for postgres..." @@ -10,12 +13,13 @@ then echo "PostgreSQL started" fi +trap - TERM -# python manage.py makemigrations api -# python manage.py flush --no-input +python manage.py makemigrations api python manage.py migrate -# create superuser for postgre admin on start up +# create superuser for postgres admin on start up python manage.py createsu # populate the database on start up python manage.py populatedb + exec "$@"