✨(backend) first working prototype
This is built from our boilerplate and adds a first working prototype of indexing (single document or bulk) and searching (with basic filtering and sorting. No authentication yet). Run `make demo` to generate an index for development.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# Python
|
||||
__pycache__
|
||||
*.pyc
|
||||
**/__pycache__
|
||||
**/*.pyc
|
||||
venv
|
||||
.venv
|
||||
|
||||
# System-specific files
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# Docker
|
||||
docker compose.*
|
||||
env.d
|
||||
|
||||
# Docs
|
||||
docs
|
||||
*.md
|
||||
*.log
|
||||
|
||||
# Development/test cache & configurations
|
||||
data
|
||||
.cache
|
||||
.circleci
|
||||
.git
|
||||
.vscode
|
||||
.iml
|
||||
.idea
|
||||
db.sqlite3
|
||||
.mypy_cache
|
||||
.pylint.d
|
||||
.pytest_cache
|
||||
|
||||
# Frontend
|
||||
node_modules
|
||||
@@ -0,0 +1,6 @@
|
||||
<!---
|
||||
Thanks for filing an issue 😄 ! Before you submit, please read the following:
|
||||
|
||||
Check the other issue templates if you are trying to submit a bug report, feature request, or question
|
||||
Search open/closed issues before submitting since someone might have asked the same thing before!
|
||||
-->
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
about: If something is not working as expected 🤔.
|
||||
|
||||
---
|
||||
|
||||
## Bug Report
|
||||
|
||||
**Problematic behavior**
|
||||
A clear and concise description of the behavior.
|
||||
|
||||
**Expected behavior/code**
|
||||
A clear and concise description of what you expected to happen (or code).
|
||||
|
||||
**Steps to Reproduce**
|
||||
1. Do this...
|
||||
2. Then this...
|
||||
3. And then the bug happens!
|
||||
|
||||
**Environment**
|
||||
- Drive version:
|
||||
- Platform:
|
||||
|
||||
**Possible Solution**
|
||||
<!--- Only if you have suggestions on a fix for the bug -->
|
||||
|
||||
**Additional context/Screenshots**
|
||||
Add any other context about the problem here. If applicable, add screenshots to help explain.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: ✨ Feature Request
|
||||
about: I have a suggestion (and may want to build it 💪)!
|
||||
|
||||
---
|
||||
|
||||
## Feature Request
|
||||
|
||||
**Is your feature request related to a problem or unsupported use case? Please describe.**
|
||||
A clear and concise description of what the problem is. For example: I need to do some task and I have an issue...
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen. Add any considered drawbacks.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Discovery, Documentation, Adoption, Migration Strategy**
|
||||
If you can, explain how users will be able to use this and possibly write out a version the docs (if applicable).
|
||||
Maybe a screenshot or design?
|
||||
|
||||
**Do you want to work on it through a Pull Request?**
|
||||
<!-- Make sure to coordinate with us before you spend too much time working on an implementation! -->
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: 🤗 Support Question
|
||||
about: If you have a question 💬, or something was not clear from the docs!
|
||||
|
||||
---
|
||||
|
||||
<!-- ^ Click "Preview" for a nicer view! ^
|
||||
We primarily use GitHub as an issue tracker. If however you're encountering an issue not covered in the docs, we may be able to help! -->
|
||||
|
||||
---
|
||||
|
||||
Please make sure you have read our [main Readme](https://github.com/numerique-gouv/drive).
|
||||
|
||||
Also make sure it was not already answered in [an open or close issue](https://github.com/numerique-gouv/drive/issues).
|
||||
|
||||
If your question was not covered, and you feel like it should be, fire away! We'd love to improve our docs! 👌
|
||||
|
||||
**Topic**
|
||||
What's the general area of your question: for example, docker setup, database schema, search functionality,...
|
||||
|
||||
**Question**
|
||||
Try to be as specific as possible so we can help you as best we can. Please be patient 🙏
|
||||
@@ -0,0 +1,11 @@
|
||||
## Purpose
|
||||
|
||||
Description...
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
Description...
|
||||
|
||||
- [] item 1...
|
||||
- [] item 2...
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'preprod'
|
||||
- 'production'
|
||||
|
||||
|
||||
jobs:
|
||||
notify-argocd:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "drive,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/drive/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
-
|
||||
name: Call argocd github webhook
|
||||
run: |
|
||||
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/'$GITHUB_REPOSITORY'"}}'
|
||||
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${ARGOCD_WEBHOOK_SECRET}'' | awk '{print "X-Hub-Signature: sha1="$2}')
|
||||
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" $ARGOCD_WEBHOOK_URL
|
||||
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${ARGOCD_PRODUCTION_WEBHOOK_SECRET}'' | awk '{print "X-Hub-Signature: sha1="$2}')
|
||||
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" $ARGOCD_PRODUCTION_WEBHOOK_URL
|
||||
|
||||
start-test-on-preprod:
|
||||
needs:
|
||||
- notify-argocd
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.event.ref, 'refs/tags/preprod')
|
||||
steps:
|
||||
-
|
||||
name: Debug
|
||||
run: |
|
||||
echo "Start test when preprod is ready"
|
||||
@@ -0,0 +1,140 @@
|
||||
name: Docker Hub Workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
env:
|
||||
DOCKER_USER: 1001:127
|
||||
|
||||
jobs:
|
||||
build-and-push-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "drive,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/drive/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: lasuite/drive-backend
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
target: backend-production
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build-and-push-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "drive,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/drive/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: lasuite/drive-frontend
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
notify-argocd:
|
||||
needs:
|
||||
- build-and-push-frontend
|
||||
- build-and-push-backend
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event_name != 'pull_request'
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "drive,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/drive/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
-
|
||||
name: Call argocd github webhook
|
||||
run: |
|
||||
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/'$GITHUB_REPOSITORY'"}}'
|
||||
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${ARGOCD_WEBHOOK_SECRET}'' | awk '{print "X-Hub-Signature: sha1="$2}')
|
||||
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" $ARGOCD_WEBHOOK_URL
|
||||
@@ -0,0 +1,203 @@
|
||||
name: Main Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
lint-git:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
run: git log
|
||||
- name: Enforce absence of print statements in code
|
||||
run: |
|
||||
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/drive.yml' | grep "print("
|
||||
- name: Check absence of fixup commits
|
||||
run: |
|
||||
! git log | grep 'fixup!'
|
||||
- name: Install gitlint
|
||||
run: pip install --user requests gitlint
|
||||
- name: Lint commit messages added to main
|
||||
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
|
||||
github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
|
||||
|
||||
lint-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Check CHANGELOG max line length
|
||||
run: |
|
||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
if [ $max_line_length -ge 80 ]; then
|
||||
echo "ERROR: CHANGELOG has lines longer than 80 characters."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
lint-back:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/backend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Check code formatting with ruff
|
||||
run: ~/.local/bin/ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: ~/.local/bin/ruff check .
|
||||
- name: Lint code with pylint
|
||||
run: ~/.local/bin/pylint .
|
||||
|
||||
test-back:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-mails
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/backend
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: drive
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5432:5432
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
env:
|
||||
DJANGO_CONFIGURATION: Test
|
||||
DJANGO_SETTINGS_MODULE: drive.settings
|
||||
DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly
|
||||
OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only
|
||||
DB_HOST: localhost
|
||||
DB_NAME: drive
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create writable /data
|
||||
run: |
|
||||
sudo mkdir -p /data/media && \
|
||||
sudo mkdir -p /data/static
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
|
||||
- name: Install gettext (required to compile messages)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext
|
||||
|
||||
- name: Generate a MO file from strings extracted from the project
|
||||
run: python manage.py compilemessages
|
||||
|
||||
- name: Run tests
|
||||
run: ~/.local/bin/pytest -n 2
|
||||
|
||||
i18n-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "infrastructure,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/drive/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
|
||||
- name: Install gettext (required to make messages)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install development dependencies
|
||||
working-directory: src/backend
|
||||
run: pip install --user .[dev]
|
||||
|
||||
- name: Generate the translation base file
|
||||
run: ~/.local/bin/django-admin makemessages --keep-pot --all
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18.x"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: src/frontend/yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/frontend/ && yarn install --frozen-lockfile
|
||||
|
||||
- name: Extract the frontend translation
|
||||
run: make frontend-i18n-extract
|
||||
|
||||
- name: Upload files to Crowdin
|
||||
run: |
|
||||
docker run \
|
||||
--rm \
|
||||
-e CROWDIN_API_TOKEN=$CROWDIN_API_TOKEN \
|
||||
-e CROWDIN_PROJECT_ID=$CROWDIN_PROJECT_ID \
|
||||
-e CROWDIN_BASE_PATH=$CROWDIN_BASE_PATH \
|
||||
-v "${{ github.workspace }}:/app" \
|
||||
crowdin/cli:3.16.0 \
|
||||
crowdin upload sources -c /app/crowdin/config.yml
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
.DS_Store
|
||||
.next/
|
||||
|
||||
# Translations # Translations
|
||||
*.pot
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
env.d/development/*
|
||||
!env.d/development/*.dist
|
||||
env.d/terraform
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Mails
|
||||
src/backend/core/templates/mail/
|
||||
|
||||
# Typescript client
|
||||
src/frontend/tsclient
|
||||
|
||||
# Swagger
|
||||
**/swagger.json
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Terraform
|
||||
.terraform
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
|
||||
# Test & lint
|
||||
.coverage
|
||||
.pylint.d
|
||||
.pytest_cache
|
||||
db.sqlite3
|
||||
.mypy_cache
|
||||
|
||||
# Site media
|
||||
/data/
|
||||
|
||||
# IDEs
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
.devcontainer
|
||||
@@ -0,0 +1,78 @@
|
||||
# All these sections are optional, edit this file as you like.
|
||||
[general]
|
||||
# Ignore certain rules, you can reference them by their id or by their full name
|
||||
# ignore=title-trailing-punctuation, T3
|
||||
|
||||
# verbosity should be a value between 1 and 3, the commandline -v flags take precedence over this
|
||||
# verbosity = 2
|
||||
|
||||
# By default gitlint will ignore merge commits. Set to 'false' to disable.
|
||||
# ignore-merge-commits=true
|
||||
|
||||
# By default gitlint will ignore fixup commits. Set to 'false' to disable.
|
||||
# ignore-fixup-commits=true
|
||||
|
||||
# By default gitlint will ignore squash commits. Set to 'false' to disable.
|
||||
# ignore-squash-commits=true
|
||||
|
||||
# Enable debug mode (prints more output). Disabled by default.
|
||||
# debug=true
|
||||
|
||||
# Set the extra-path where gitlint will search for user defined rules
|
||||
# See http://jorisroovers.github.io/gitlint/user_defined_rules for details
|
||||
extra-path=gitlint/
|
||||
|
||||
# [title-max-length]
|
||||
# line-length=80
|
||||
|
||||
[title-must-not-contain-word]
|
||||
# Comma-separated list of words that should not occur in the title. Matching is case
|
||||
# insensitive. It's fine if the keyword occurs as part of a larger word (so "WIPING"
|
||||
# will not cause a violation, but "WIP: my title" will.
|
||||
words=wip
|
||||
|
||||
#[title-match-regex]
|
||||
# python like regex (https://docs.python.org/2/library/re.html) that the
|
||||
# commit-msg title must be matched to.
|
||||
# Note that the regex can contradict with other rules if not used correctly
|
||||
# (e.g. title-must-not-contain-word).
|
||||
#regex=
|
||||
|
||||
# [B1]
|
||||
# B1 = body-max-line-length
|
||||
# line-length=120
|
||||
# [body-min-length]
|
||||
# min-length=5
|
||||
|
||||
# [body-is-missing]
|
||||
# Whether to ignore this rule on merge commits (which typically only have a title)
|
||||
# default = True
|
||||
# ignore-merge-commits=false
|
||||
|
||||
# [body-changed-file-mention]
|
||||
# List of files that need to be explicitly mentioned in the body when they are changed
|
||||
# This is useful for when developers often erroneously edit certain files or git submodules.
|
||||
# By specifying this rule, developers can only change the file when they explicitly reference
|
||||
# it in the commit message.
|
||||
# files=gitlint/rules.py,README.md
|
||||
|
||||
# [author-valid-email]
|
||||
# python like regex (https://docs.python.org/2/library/re.html) that the
|
||||
# commit author email address should be matched to
|
||||
# For example, use the following regex if you only want to allow email addresses from foo.com
|
||||
# regex=[^@]+@foo.com
|
||||
|
||||
[ignore-by-title]
|
||||
# Allow empty body & wrong title pattern only when bots (pyup/greenkeeper)
|
||||
# upgrade dependencies
|
||||
regex=^(⬆️.*|Update (.*) from (.*) to (.*)|(chore|fix)\(package\): update .*)$
|
||||
ignore=B6,UC1
|
||||
|
||||
# [ignore-by-body]
|
||||
# Ignore certain rules for commits of which the body has a line that matches a regex
|
||||
# E.g. Match bodies that have a line that that contain "release"
|
||||
# regex=(.*)release(.*)
|
||||
#
|
||||
# Ignore certain rules, you can reference them by their id or by their full name
|
||||
# Use 'all' to ignore all rules
|
||||
# ignore=T1,body-min-length
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "secrets"]
|
||||
path = secrets
|
||||
url = ../secrets
|
||||
@@ -0,0 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0),
|
||||
and this project adheres to
|
||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
# Django drive
|
||||
|
||||
# ---- base image to inherit from ----
|
||||
FROM python:3.10-slim-bullseye AS base
|
||||
|
||||
# Upgrade pip to its latest release to speed up dependencies installation
|
||||
RUN python -m pip install --upgrade pip
|
||||
|
||||
# Upgrade system packages to install security updates
|
||||
RUN apt-get update && \
|
||||
apt-get -y upgrade && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ---- Back-end builder image ----
|
||||
FROM base AS back-builder
|
||||
|
||||
WORKDIR /builder
|
||||
|
||||
# Copy required python dependencies
|
||||
COPY ./src/backend /builder
|
||||
|
||||
RUN mkdir /install && \
|
||||
pip install --prefix=/install .
|
||||
|
||||
# ---- static link collector ----
|
||||
FROM base AS link-collector
|
||||
ARG DRIVE_STATIC_ROOT=/data/static
|
||||
|
||||
# Install libpangocairo & rdfind
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
libpangocairo-1.0-0 \
|
||||
rdfind && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy installed python dependencies
|
||||
COPY --from=back-builder /install /usr/local
|
||||
|
||||
# Copy drive application (see .dockerignore)
|
||||
COPY ./src/backend /app/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# collectstatic
|
||||
RUN DJANGO_CONFIGURATION=Build DJANGO_JWT_PRIVATE_SIGNING_KEY=Dummy \
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
# Replace duplicated file by a symlink to decrease the overall size of the
|
||||
# final image
|
||||
RUN rdfind -makesymlinks true -followsymlinks true -makeresultsfile false ${DRIVE_STATIC_ROOT}
|
||||
|
||||
# ---- Core application image ----
|
||||
FROM base AS core
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Install required system libs
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
gettext \
|
||||
libcairo2 \
|
||||
libffi-dev \
|
||||
libgdk-pixbuf2.0-0 \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
shared-mime-info && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy entrypoint
|
||||
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||
|
||||
# Give the "root" group the same permissions as the "root" user on /etc/passwd
|
||||
# to allow a user belonging to the root group to add new users; typically the
|
||||
# docker user (see entrypoint).
|
||||
RUN chmod g=u /etc/passwd
|
||||
|
||||
# Copy installed python dependencies
|
||||
COPY --from=back-builder /install /usr/local
|
||||
|
||||
# Copy drive application (see .dockerignore)
|
||||
COPY ./src/backend /app/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# We wrap commands run in this container by the following entrypoint that
|
||||
# creates a user on-the-fly with the container user ID (see USER) and root group
|
||||
# ID.
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
|
||||
|
||||
# ---- Development image ----
|
||||
FROM core AS backend-development
|
||||
|
||||
# Switch back to the root user to install development dependencies
|
||||
USER root:root
|
||||
|
||||
# Install psql
|
||||
RUN apt-get update && \
|
||||
apt-get install -y postgresql-client && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Uninstall drive and re-install it in editable mode along with development
|
||||
# dependencies
|
||||
RUN pip uninstall -y drive
|
||||
RUN pip install -e .[dev]
|
||||
|
||||
# Restore the un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
# Target database host (e.g. database engine following docker compose services
|
||||
# name) & port
|
||||
ENV DB_HOST=postgresql \
|
||||
DB_PORT=5432
|
||||
|
||||
# Run django development server
|
||||
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||
|
||||
# ---- Production image ----
|
||||
FROM core AS backend-production
|
||||
|
||||
ARG DRIVE_STATIC_ROOT=/data/static
|
||||
|
||||
# Gunicorn
|
||||
RUN mkdir -p /usr/local/etc/gunicorn
|
||||
COPY docker/files/usr/local/etc/gunicorn/drive.py /usr/local/etc/gunicorn/drive.py
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
# Copy statics
|
||||
COPY --from=link-collector ${DRIVE_STATIC_ROOT} ${DRIVE_STATIC_ROOT}
|
||||
|
||||
# Copy drive mails
|
||||
COPY --from=mail-builder /mail/backend/core/templates/mail /app/core/templates/mail
|
||||
|
||||
# The default command runs gunicorn WSGI server in drive's main module
|
||||
CMD ["gunicorn", "-c", "/usr/local/etc/gunicorn/drive.py", "drive.wsgi:application"]
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Direction Interministérielle du Numérique - Gouvernement Français
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,265 @@
|
||||
# /!\ /!\ /!\ /!\ /!\ /!\ /!\ DISCLAIMER /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\
|
||||
#
|
||||
# This Makefile is only meant to be used for DEVELOPMENT purpose as we are
|
||||
# changing the user id that will run in the container.
|
||||
#
|
||||
# PLEASE DO NOT USE IT FOR YOUR CI/PRODUCTION/WHATEVER...
|
||||
#
|
||||
# /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\
|
||||
#
|
||||
# Note to developers:
|
||||
#
|
||||
# While editing this file, please respect the following statements:
|
||||
#
|
||||
# 1. Every variable should be defined in the ad hoc VARIABLES section with a
|
||||
# relevant subsection
|
||||
# 2. Every new rule should be defined in the ad hoc RULES section with a
|
||||
# relevant subsection depending on the targeted service
|
||||
# 3. Rules should be sorted alphabetically within their section
|
||||
# 4. When a rule has multiple dependencies, you should:
|
||||
# - duplicate the rule name to add the help string (if required)
|
||||
# - write one dependency per line to increase readability and diffs
|
||||
# 5. .PHONY rule statement should be written after the corresponding rule
|
||||
# ==============================================================================
|
||||
# VARIABLES
|
||||
|
||||
BOLD := \033[1m
|
||||
RESET := \033[0m
|
||||
GREEN := \033[1;32m
|
||||
|
||||
|
||||
# -- Database
|
||||
|
||||
DB_HOST = postgresql
|
||||
DB_PORT = 5432
|
||||
|
||||
# -- Docker
|
||||
# Get the current user ID to use for docker run and docker exec commands
|
||||
DOCKER_UID = $(shell id -u)
|
||||
DOCKER_GID = $(shell id -g)
|
||||
DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
|
||||
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
|
||||
COMPOSE_EXEC = $(COMPOSE) exec
|
||||
COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app
|
||||
COMPOSE_RUN = $(COMPOSE) run --rm
|
||||
COMPOSE_RUN_APP = $(COMPOSE_RUN) app
|
||||
COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin
|
||||
WAIT_DB = @$(COMPOSE_RUN) dockerize -wait tcp://$(DB_HOST):$(DB_PORT) -timeout 60s
|
||||
|
||||
# -- Backend
|
||||
MANAGE = $(COMPOSE_RUN_APP) python manage.py
|
||||
|
||||
# ==============================================================================
|
||||
# RULES
|
||||
|
||||
default: help
|
||||
|
||||
data/opensearch:
|
||||
@mkdir -p data/opensearch
|
||||
|
||||
data/static:
|
||||
@mkdir -p data/static
|
||||
|
||||
# -- Project
|
||||
|
||||
create-env-files: ## Copy the dist env files to env files
|
||||
create-env-files: \
|
||||
env.d/development/common \
|
||||
env.d/development/crowdin \
|
||||
env.d/development/postgresql
|
||||
.PHONY: create-env-files
|
||||
|
||||
bootstrap: ## Prepare Docker images for the project
|
||||
bootstrap: \
|
||||
data/opensearch \
|
||||
data/static \
|
||||
create-env-files \
|
||||
build \
|
||||
migrate \
|
||||
demo \
|
||||
back-i18n-compile
|
||||
.PHONY: bootstrap
|
||||
|
||||
# -- Docker/compose
|
||||
build: ## build the app container
|
||||
@$(COMPOSE) build app --no-cache
|
||||
.PHONY: build
|
||||
|
||||
down: ## stop and remove containers, networks, images, and volumes
|
||||
@$(COMPOSE) down
|
||||
.PHONY: down
|
||||
|
||||
logs: ## display app logs (follow mode)
|
||||
@$(COMPOSE) logs -f app
|
||||
.PHONY: logs
|
||||
|
||||
run: ## start the wsgi (production) and development server
|
||||
@$(COMPOSE) up --force-recreate -d celery
|
||||
@echo "Wait for postgresql to be up..."
|
||||
@$(WAIT_DB)
|
||||
.PHONY: run
|
||||
|
||||
status: ## an alias for "docker compose ps"
|
||||
@$(COMPOSE) ps
|
||||
.PHONY: status
|
||||
|
||||
stop: ## stop the development server using Docker
|
||||
@$(COMPOSE) stop
|
||||
.PHONY: stop
|
||||
|
||||
# -- Backend
|
||||
|
||||
demo: ## flush db then create a demo for load testing purpose
|
||||
@$(MAKE) resetdb
|
||||
@$(MANAGE) create_demo
|
||||
.PHONY: demo
|
||||
|
||||
# Nota bene: Black should come after isort just in case they don't agree...
|
||||
lint: ## lint back-end python sources
|
||||
lint: \
|
||||
lint-ruff-format \
|
||||
lint-ruff-check \
|
||||
lint-pylint
|
||||
.PHONY: lint
|
||||
|
||||
lint-ruff-format: ## format back-end python sources with ruff
|
||||
@echo 'lint:ruff-format started…'
|
||||
@$(COMPOSE_RUN_APP) ruff format .
|
||||
.PHONY: lint-ruff-format
|
||||
|
||||
lint-ruff-check: ## lint back-end python sources with ruff
|
||||
@echo 'lint:ruff-check started…'
|
||||
@$(COMPOSE_RUN_APP) ruff check . --fix
|
||||
.PHONY: lint-ruff-check
|
||||
|
||||
lint-pylint: ## lint back-end python sources with pylint only on changed files from main
|
||||
@echo 'lint:pylint started…'
|
||||
bin/pylint .
|
||||
.PHONY: lint-pylint
|
||||
|
||||
test: ## run project tests
|
||||
@$(MAKE) test-back-parallel
|
||||
.PHONY: test
|
||||
|
||||
test-back: ## run back-end tests
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
bin/pytest $${args:-${1}}
|
||||
.PHONY: test-back
|
||||
|
||||
test-back-parallel: ## run all back-end tests in parallel
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
bin/pytest -n auto $${args:-${1}}
|
||||
.PHONY: test-back-parallel
|
||||
|
||||
makemigrations: ## run django makemigrations for the drive project.
|
||||
@echo "$(BOLD)Running makemigrations$(RESET)"
|
||||
@$(COMPOSE) up -d postgresql
|
||||
@$(WAIT_DB)
|
||||
@$(MANAGE) makemigrations
|
||||
.PHONY: makemigrations
|
||||
|
||||
migrate: ## run django migrations for the drive project.
|
||||
@echo "$(BOLD)Running migrations$(RESET)"
|
||||
@$(COMPOSE) up -d postgresql
|
||||
@$(WAIT_DB)
|
||||
@$(MANAGE) migrate
|
||||
.PHONY: migrate
|
||||
|
||||
superuser: ## Create an admin superuser with password "admin"
|
||||
@echo "$(BOLD)Creating a Django superuser$(RESET)"
|
||||
@$(WAIT_DB)
|
||||
@$(MANAGE) shell -c "from core.models import User; not User.objects.filter(username='admin').exists() and User.objects.create_superuser('admin', 'admin@example.com', 'admin')"
|
||||
.PHONY: superuser
|
||||
|
||||
back-i18n-compile: ## compile the gettext files
|
||||
@$(MANAGE) compilemessages --ignore="venv/**/*"
|
||||
.PHONY: back-i18n-compile
|
||||
|
||||
back-i18n-generate: ## create the .pot files used for i18n
|
||||
@$(MANAGE) makemessages -a --keep-pot
|
||||
.PHONY: back-i18n-generate
|
||||
|
||||
shell: ## connect to database shell
|
||||
@$(MANAGE) shell #_plus
|
||||
.PHONY: dbshell
|
||||
|
||||
# -- Database
|
||||
|
||||
dbshell: ## connect to database shell
|
||||
docker compose exec app python manage.py dbshell
|
||||
.PHONY: dbshell
|
||||
|
||||
resetdb: FLUSH_ARGS ?=
|
||||
resetdb: ## flush database and create a superuser "admin"
|
||||
@echo "$(BOLD)Flush database$(RESET)"
|
||||
@$(MANAGE) flush $(FLUSH_ARGS)
|
||||
@${MAKE} superuser
|
||||
.PHONY: resetdb
|
||||
|
||||
env.d/development/common:
|
||||
cp -n env.d/development/common.dist env.d/development/common
|
||||
|
||||
env.d/development/postgresql:
|
||||
cp -n env.d/development/postgresql.dist env.d/development/postgresql
|
||||
|
||||
# -- Internationalization
|
||||
|
||||
env.d/development/crowdin:
|
||||
cp -n env.d/development/crowdin.dist env.d/development/crowdin
|
||||
|
||||
crowdin-download: ## Download translated message from crowdin
|
||||
@$(COMPOSE_RUN_CROWDIN) download -c crowdin/config.yml
|
||||
.PHONY: crowdin-download
|
||||
|
||||
crowdin-download-sources: ## Download sources from Crowdin
|
||||
@$(COMPOSE_RUN_CROWDIN) download sources -c crowdin/config.yml
|
||||
.PHONY: crowdin-download-sources
|
||||
|
||||
crowdin-upload: ## Upload source translations to crowdin
|
||||
@$(COMPOSE_RUN_CROWDIN) upload sources -c crowdin/config.yml
|
||||
.PHONY: crowdin-upload
|
||||
|
||||
i18n-compile: ## compile all translations
|
||||
i18n-compile: \
|
||||
back-i18n-compile \
|
||||
frontend-i18n-compile
|
||||
.PHONY: i18n-compile
|
||||
|
||||
i18n-generate: ## create the .pot files and extract frontend messages
|
||||
i18n-generate: \
|
||||
back-i18n-generate \
|
||||
frontend-i18n-generate
|
||||
.PHONY: i18n-generate
|
||||
|
||||
i18n-download-and-compile: ## download all translated messages and compile them to be used by all applications
|
||||
i18n-download-and-compile: \
|
||||
crowdin-download \
|
||||
i18n-compile
|
||||
.PHONY: i18n-download-and-compile
|
||||
|
||||
i18n-generate-and-upload: ## generate source translations for all applications and upload them to Crowdin
|
||||
i18n-generate-and-upload: \
|
||||
i18n-generate \
|
||||
crowdin-upload
|
||||
.PHONY: i18n-generate-and-upload
|
||||
|
||||
# -- Misc
|
||||
clean: ## restore repository state as it was freshly cloned
|
||||
git clean -idx
|
||||
.PHONY: clean
|
||||
|
||||
help:
|
||||
@echo "$(BOLD)drive Makefile"
|
||||
@echo "Please use 'make $(BOLD)target$(RESET)' where $(BOLD)target$(RESET) is one of:"
|
||||
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(GREEN)%-30s$(RESET) %s\n", $$1, $$2}'
|
||||
.PHONY: help
|
||||
|
||||
# -- K8S
|
||||
build-k8s-cluster: ## build the kubernetes cluster using kind
|
||||
./bin/start-kind.sh
|
||||
.PHONY: build-k8s-cluster
|
||||
|
||||
start-tilt: ## start the kubernetes cluster using kind
|
||||
tilt up -f ./bin/Tiltfile
|
||||
.PHONY: build-k8s-cluster
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Upgrade
|
||||
|
||||
All instructions to upgrade this project from one release to the next will be
|
||||
documented in this file. Upgrades must be run sequentially, meaning you should
|
||||
not skip minor/major releases while upgrading (fix releases can be skipped).
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
For most upgrades, you just need to run the django migrations with
|
||||
the following command inside your docker container:
|
||||
|
||||
`python manage.py migrate`
|
||||
|
||||
(Note : in your development environment, you can `make migrate`.)
|
||||
|
||||
## [Unreleased]
|
||||
@@ -0,0 +1,57 @@
|
||||
load('ext://uibutton', 'cmd_button', 'bool_input', 'location')
|
||||
load('ext://namespace', 'namespace_create', 'namespace_inject')
|
||||
namespace_create('drive')
|
||||
|
||||
docker_build(
|
||||
'localhost:5001/drive-backend:latest',
|
||||
context='..',
|
||||
dockerfile='../Dockerfile',
|
||||
only=['./src/backend', './docker'],
|
||||
target = 'backend-production',
|
||||
live_update=[
|
||||
sync('../src/backend', '/app'),
|
||||
run(
|
||||
'pip install -r /app/requirements.txt',
|
||||
trigger=['./api/requirements.txt']
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
docker_build(
|
||||
'localhost:5001/drive-frontend:latest',
|
||||
context='..',
|
||||
dockerfile='../src/frontend/Dockerfile',
|
||||
only=['./src/frontend', './docker', './.dockerignore'],
|
||||
target = 'drive',
|
||||
live_update=[
|
||||
sync('../src/frontend', '/home/frontend'),
|
||||
]
|
||||
)
|
||||
|
||||
k8s_yaml(local('cd ../src/helm && helmfile -n drive -e dev template .'))
|
||||
|
||||
migration = '''
|
||||
set -eu
|
||||
# get k8s pod name from tilt resource name
|
||||
POD_NAME="$(tilt get kubernetesdiscovery drive-backend -ojsonpath='{.status.pods[0].name}')"
|
||||
kubectl -n drive exec "$POD_NAME" -- python manage.py makemigrations
|
||||
'''
|
||||
cmd_button('Make migration',
|
||||
argv=['sh', '-c', migration],
|
||||
resource='drive-backend',
|
||||
icon_name='developer_board',
|
||||
text='Run makemigration',
|
||||
)
|
||||
|
||||
pod_migrate = '''
|
||||
set -eu
|
||||
# get k8s pod name from tilt resource name
|
||||
POD_NAME="$(tilt get kubernetesdiscovery drive-backend -ojsonpath='{.status.pods[0].name}')"
|
||||
kubectl -n drive exec "$POD_NAME" -- python manage.py migrate --no-input
|
||||
'''
|
||||
cmd_button('Migrate db',
|
||||
argv=['sh', '-c', pod_migrate],
|
||||
resource='drive-backend',
|
||||
icon_name='developer_board',
|
||||
text='Run database migration',
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd)"
|
||||
UNSET_USER=0
|
||||
|
||||
COMPOSE_FILE="${REPO_DIR}/docker-compose.yml"
|
||||
COMPOSE_PROJECT="drive"
|
||||
|
||||
|
||||
# _set_user: set (or unset) default user id used to run docker commands
|
||||
#
|
||||
# usage: _set_user
|
||||
#
|
||||
# You can override default user ID (the current host user ID), by defining the
|
||||
# USER_ID environment variable.
|
||||
#
|
||||
# To avoid running docker commands with a custom user, please set the
|
||||
# $UNSET_USER environment variable to 1.
|
||||
function _set_user() {
|
||||
|
||||
if [ $UNSET_USER -eq 1 ]; then
|
||||
USER_ID=""
|
||||
return
|
||||
fi
|
||||
|
||||
# USER_ID = USER_ID or `id -u` if USER_ID is not set
|
||||
USER_ID=${USER_ID:-$(id -u)}
|
||||
|
||||
echo "🙋(user) ID: ${USER_ID}"
|
||||
}
|
||||
|
||||
# docker_compose: wrap docker compose command
|
||||
#
|
||||
# usage: docker_compose [options] [ARGS...]
|
||||
#
|
||||
# options: docker compose command options
|
||||
# ARGS : docker compose command arguments
|
||||
function _docker_compose() {
|
||||
|
||||
echo "🐳(compose) project: '${COMPOSE_PROJECT}' file: '${COMPOSE_FILE}'"
|
||||
docker compose \
|
||||
-p "${COMPOSE_PROJECT}" \
|
||||
-f "${COMPOSE_FILE}" \
|
||||
--project-directory "${REPO_DIR}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
# _dc_run: wrap docker compose run command
|
||||
#
|
||||
# usage: _dc_run [options] [ARGS...]
|
||||
#
|
||||
# options: docker compose run command options
|
||||
# ARGS : docker compose run command arguments
|
||||
function _dc_run() {
|
||||
_set_user
|
||||
|
||||
user_args="--user=$USER_ID"
|
||||
if [ -z $USER_ID ]; then
|
||||
user_args=""
|
||||
fi
|
||||
|
||||
_docker_compose run --rm $user_args "$@"
|
||||
}
|
||||
|
||||
# _dc_exec: wrap docker compose exec command
|
||||
#
|
||||
# usage: _dc_exec [options] [ARGS...]
|
||||
#
|
||||
# options: docker compose exec command options
|
||||
# ARGS : docker compose exec command arguments
|
||||
function _dc_exec() {
|
||||
_set_user
|
||||
|
||||
echo "🐳(compose) exec command: '\$@'"
|
||||
|
||||
user_args="--user=$USER_ID"
|
||||
if [ -z $USER_ID ]; then
|
||||
user_args=""
|
||||
fi
|
||||
|
||||
_docker_compose exec $user_args "$@"
|
||||
}
|
||||
|
||||
# _django_manage: wrap django's manage.py command with docker compose
|
||||
#
|
||||
# usage : _django_manage [ARGS...]
|
||||
#
|
||||
# ARGS : django's manage.py command arguments
|
||||
function _django_manage() {
|
||||
_dc_run app python manage.py "$@"
|
||||
}
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# shellcheck source=bin/_config.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
_docker_compose "$@"
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# shellcheck source=bin/_config.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
_django_manage "$@"
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# shellcheck source=bin/_config.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
declare diff_from
|
||||
declare -a paths
|
||||
declare -a args
|
||||
|
||||
# Parse options
|
||||
for arg in "$@"
|
||||
do
|
||||
case $arg in
|
||||
--diff-only=*)
|
||||
diff_from="${arg#*=}"
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
args+=("$arg")
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
paths+=("$arg")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "${diff_from}" ]]; then
|
||||
# Run pylint only on modified files located in src/backend
|
||||
# (excluding deleted files and migration files)
|
||||
# shellcheck disable=SC2207
|
||||
paths=($(git diff "${diff_from}" --name-only --diff-filter=d -- src/backend ':!**/migrations/*.py' | grep -E '^src/backend/.*\.py$'))
|
||||
fi
|
||||
|
||||
# Fix docker vs local path when project sources are mounted as a volume
|
||||
read -ra paths <<< "$(echo "${paths[@]}" | sed "s|src/backend/||g")"
|
||||
_dc_run app pylint "${paths[@]}" "${args[@]}"
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
_dc_run \
|
||||
-e DJANGO_CONFIGURATION=Test \
|
||||
app \
|
||||
pytest "$@"
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/bin/sh
|
||||
set -o errexit
|
||||
|
||||
CURRENT_DIR=$(pwd)
|
||||
|
||||
echo "0. Create ca"
|
||||
# 0. Create ca
|
||||
mkcert -install
|
||||
cd /tmp
|
||||
mkcert "127.0.0.1.nip.io" "*.127.0.0.1.nip.io"
|
||||
cd $CURRENT_DIR
|
||||
|
||||
echo "1. Create registry container unless it already exists"
|
||||
# 1. Create registry container unless it already exists
|
||||
reg_name='kind-registry'
|
||||
reg_port='5001'
|
||||
if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then
|
||||
docker run \
|
||||
-d --restart=always -p "127.0.0.1:${reg_port}:5000" --network bridge --name "${reg_name}" \
|
||||
registry:2
|
||||
fi
|
||||
|
||||
echo "2. Create kind cluster with containerd registry config dir enabled"
|
||||
# 2. Create kind cluster with containerd registry config dir enabled
|
||||
# TODO: kind will eventually enable this by default and this patch will
|
||||
# be unnecessary.
|
||||
#
|
||||
# See:
|
||||
# https://github.com/kubernetes-sigs/kind/issues/2875
|
||||
# https://github.com/containerd/containerd/blob/main/docs/cri/config.md#registry-configuration
|
||||
# See: https://github.com/containerd/containerd/blob/main/docs/hosts.md
|
||||
cat <<EOF | kind create cluster --config=-
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
containerdConfigPatches:
|
||||
- |-
|
||||
[plugins."io.containerd.grpc.v1.cri".registry]
|
||||
config_path = "/etc/containerd/certs.d"
|
||||
nodes:
|
||||
- role: control-plane
|
||||
image: kindest/node:v1.27.3
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
kind: InitConfiguration
|
||||
nodeRegistration:
|
||||
kubeletExtraArgs:
|
||||
node-labels: "ingress-ready=true"
|
||||
extraPortMappings:
|
||||
- containerPort: 80
|
||||
hostPort: 80
|
||||
protocol: TCP
|
||||
- containerPort: 443
|
||||
hostPort: 443
|
||||
protocol: TCP
|
||||
- role: worker
|
||||
image: kindest/node:v1.27.3
|
||||
- role: worker
|
||||
image: kindest/node:v1.27.3
|
||||
EOF
|
||||
|
||||
echo "3. Add the registry config to the nodes"
|
||||
# 3. Add the registry config to the nodes
|
||||
#
|
||||
# This is necessary because localhost resolves to loopback addresses that are
|
||||
# network-namespace local.
|
||||
# In other words: localhost in the container is not localhost on the host.
|
||||
#
|
||||
# We want a consistent name that works from both ends, so we tell containerd to
|
||||
# alias localhost:${reg_port} to the registry container when pulling images
|
||||
REGISTRY_DIR="/etc/containerd/certs.d/localhost:${reg_port}"
|
||||
for node in $(kind get nodes); do
|
||||
docker exec "${node}" mkdir -p "${REGISTRY_DIR}"
|
||||
cat <<EOF | docker exec -i "${node}" cp /dev/stdin "${REGISTRY_DIR}/hosts.toml"
|
||||
[host."http://${reg_name}:5000"]
|
||||
EOF
|
||||
done
|
||||
|
||||
echo "4. Connect the registry to the cluster network if not already connected"
|
||||
# 4. Connect the registry to the cluster network if not already connected
|
||||
# This allows kind to bootstrap the network but ensures they're on the same network
|
||||
if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${reg_name}")" = 'null' ]; then
|
||||
docker network connect "kind" "${reg_name}"
|
||||
fi
|
||||
|
||||
echo "5. Document the local registry"
|
||||
# 5. Document the local registry
|
||||
# https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: local-registry-hosting
|
||||
namespace: kube-public
|
||||
data:
|
||||
localRegistryHosting.v1: |
|
||||
host: "localhost:${reg_port}"
|
||||
help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
|
||||
EOF
|
||||
|
||||
echo "6. Install ingress-nginx"
|
||||
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
|
||||
kubectl -n ingress-nginx create secret tls mkcert --key /tmp/127.0.0.1.nip.io+1-key.pem --cert /tmp/127.0.0.1.nip.io+1.pem
|
||||
kubectl -n ingress-nginx patch deployments.apps ingress-nginx-controller --type 'json' -p '[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value":"--default-ssl-certificate=ingress-nginx/mkcert"}]'
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
project=$(_set_openstack_project)
|
||||
echo "Using \"${project}\" project..."
|
||||
|
||||
source "${TERRAFORM_DIRECTORY}/${project}/openrc.sh"
|
||||
|
||||
# Run Terraform commands in the Hashicorp docker container via docker compose
|
||||
# shellcheck disable=SC2068
|
||||
DOCKER_USER="$(id -u):$(id -g)" \
|
||||
PROJECT="${project}" \
|
||||
docker compose run --rm \
|
||||
-e OS_AUTH_URL \
|
||||
-e OS_IDENTITY_API_VERSION \
|
||||
-e OS_USER_DOMAIN_NAME \
|
||||
-e OS_PROJECT_DOMAIN_NAME \
|
||||
-e OS_TENANT_ID \
|
||||
-e OS_TENANT_NAME \
|
||||
-e OS_USERNAME \
|
||||
-e OS_PASSWORD \
|
||||
-e OS_REGION_NAME \
|
||||
terraform-state "$@"
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
_dc_run \
|
||||
-e DJANGO_CONFIGURATION=Test \
|
||||
app \
|
||||
python manage.py spectacular \
|
||||
--api-version 'v1.0' \
|
||||
--urlconf 'drive.api_urls' \
|
||||
--format openapi-json \
|
||||
--file /app/core/tests/swagger/swagger.json
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
find . -name "*.enc.*" -exec sops updatekeys -y {} \;
|
||||
@@ -0,0 +1,29 @@
|
||||
#
|
||||
# Your crowdin's credentials
|
||||
#
|
||||
api_token_env: CROWDIN_API_TOKEN
|
||||
project_id_env: CROWDIN_PROJECT_ID
|
||||
base_path_env: CROWDIN_BASE_PATH
|
||||
|
||||
#
|
||||
# Choose file structure in crowdin
|
||||
# e.g. true or false
|
||||
#
|
||||
preserve_hierarchy: true
|
||||
|
||||
#
|
||||
# Files configuration
|
||||
#
|
||||
files: [
|
||||
{
|
||||
source : "/backend/locale/django.pot",
|
||||
dest: "/backend-drive.pot",
|
||||
translation : "/backend/locale/%locale_with_underscore%/LC_MESSAGES/django.po"
|
||||
},
|
||||
{
|
||||
source: "/frontend/packages/i18n/locales/drive/translations-crowdin.json",
|
||||
dest: "/frontend-drive.json",
|
||||
translation: "/frontend/packages/i18n/locales/drive/%two_letters_code%/translations.json",
|
||||
skip_untranslated_strings: true,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,96 @@
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:16
|
||||
env_file:
|
||||
- env.d/development/postgresql
|
||||
ports:
|
||||
- "15432:5432"
|
||||
|
||||
opensearch:
|
||||
image: opensearchproject/opensearch:latest
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- plugins.security.disabled=true
|
||||
- plugins.security.ssl.http.enabled=false
|
||||
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=drive.PASS123
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
volumes:
|
||||
- ./data/opensearch:/usr/share/opensearch/data
|
||||
ports:
|
||||
- "9200:9200"
|
||||
- "9600:9600"
|
||||
|
||||
opensearch-dashboards:
|
||||
image: opensearchproject/opensearch-dashboards:latest
|
||||
ports:
|
||||
- "5601:5601"
|
||||
environment:
|
||||
OPENSEARCH_HOSTS: '["http://opensearch:9200"]'
|
||||
DISABLE_SECURITY_DASHBOARDS_PLUGIN: "true"
|
||||
depends_on:
|
||||
- opensearch
|
||||
|
||||
redis:
|
||||
image: redis:5
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
target: backend-development
|
||||
args:
|
||||
DOCKER_USER: ${DOCKER_USER:-1000}
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: drive:backend-development
|
||||
environment:
|
||||
- PYLINTHOME=/app/.pylint.d
|
||||
- DJANGO_CONFIGURATION=Development
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/postgresql
|
||||
ports:
|
||||
- "8071:8000"
|
||||
volumes:
|
||||
- ./src/backend:/app
|
||||
- ./data/static:/data/static
|
||||
depends_on:
|
||||
- postgresql
|
||||
- opensearch
|
||||
- redis
|
||||
|
||||
celery:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: drive:backend-development
|
||||
command: ["celery", "-A", "drive.celery_app", "worker", "-l", "DEBUG"]
|
||||
environment:
|
||||
- DJANGO_CONFIGURATION=Development
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/postgresql
|
||||
volumes:
|
||||
- ./src/backend:/app
|
||||
- ./data/static:/data/static
|
||||
depends_on:
|
||||
- app
|
||||
|
||||
dockerize:
|
||||
image: jwilder/dockerize
|
||||
|
||||
crowdin:
|
||||
image: crowdin/cli:3.16.0
|
||||
volumes:
|
||||
- ".:/app"
|
||||
env_file:
|
||||
- env.d/development/crowdin
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
working_dir: /app
|
||||
|
||||
node:
|
||||
image: node:18
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
environment:
|
||||
HOME: /tmp
|
||||
volumes:
|
||||
- ".:/app"
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# The container user (see USER in the Dockerfile) is an un-privileged user that
|
||||
# does not exists and is not created during the build phase (see Dockerfile).
|
||||
# Hence, we use this entrypoint to wrap commands that will be run in the
|
||||
# container to create an entry for this user in the /etc/passwd file.
|
||||
#
|
||||
# The following environment variables may be passed to the container to
|
||||
# customize running user account:
|
||||
#
|
||||
# * USER_NAME: container user name (default: default)
|
||||
# * HOME : container user home directory (default: none)
|
||||
#
|
||||
# To pass environment variables, you can either use the -e option of the docker run command:
|
||||
#
|
||||
# docker run --rm -e USER_NAME=foo -e HOME='/home/foo' drive:latest python manage.py migrate
|
||||
#
|
||||
# or define new variables in an environment file to use with docker or docker compose:
|
||||
#
|
||||
# # env.d/production
|
||||
# USER_NAME=foo
|
||||
# HOME=/home/foo
|
||||
#
|
||||
# docker run --rm --env-file env.d/production drive:latest python manage.py migrate
|
||||
#
|
||||
|
||||
echo "🐳(entrypoint) creating user running in the container..."
|
||||
if ! whoami > /dev/null 2>&1; then
|
||||
if [ -w /etc/passwd ]; then
|
||||
echo "${USER_NAME:-default}:x:$(id -u):$(id -g):${USER_NAME:-default} user:${HOME}:/sbin/nologin" >> /etc/passwd
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "🐳(entrypoint) running your command: ${*}"
|
||||
exec "$@"
|
||||
@@ -0,0 +1,16 @@
|
||||
# Gunicorn-django settings
|
||||
bind = ["0.0.0.0:8000"]
|
||||
name = "drive"
|
||||
python_path = "/app"
|
||||
|
||||
# Run
|
||||
graceful_timeout = 90
|
||||
timeout = 90
|
||||
workers = 3
|
||||
|
||||
# Logging
|
||||
# Using '-' for the access log file makes gunicorn log accesses to stdout
|
||||
accesslog = "-"
|
||||
# Using '-' for the error log file makes gunicorn log errors to stderr
|
||||
errorlog = "-"
|
||||
loglevel = "info"
|
||||
@@ -0,0 +1,31 @@
|
||||
# Django
|
||||
DJANGO_ALLOWED_HOSTS=*
|
||||
DJANGO_SECRET_KEY=ThisIsAnExampleKeyForDevPurposeOnly
|
||||
DJANGO_SETTINGS_MODULE=drive.settings
|
||||
DJANGO_SUPERUSER_PASSWORD=admin
|
||||
|
||||
# Python
|
||||
PYTHONPATH=/app
|
||||
|
||||
# drive settings
|
||||
|
||||
# Backend url
|
||||
DRIVE_BASE_URL="http://localhost:8072"
|
||||
|
||||
# Opensearch
|
||||
OPENSEARCH_PASSWORD=drive.PASS123
|
||||
OPENSEARCH_USE_SSL=false
|
||||
|
||||
# OIDC
|
||||
OIDC_OP_JWKS_ENDPOINT=http://nginx:8083/realms/drive/protocol/openid-connect/certs
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT=http://localhost:8083/realms/drive/protocol/openid-connect/auth
|
||||
OIDC_OP_TOKEN_ENDPOINT=http://nginx:8083/realms/drive/protocol/openid-connect/token
|
||||
OIDC_OP_USER_ENDPOINT=http://nginx:8083/realms/drive/protocol/openid-connect/userinfo
|
||||
|
||||
OIDC_RP_CLIENT_ID=drive
|
||||
OIDC_RP_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
|
||||
OIDC_RP_SIGN_ALGO=RS256
|
||||
OIDC_RP_SCOPES="openid email"
|
||||
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
|
||||
@@ -0,0 +1,3 @@
|
||||
CROWDIN_API_TOKEN=Your-Api-Token
|
||||
CROWDIN_PROJECT_ID=Your-Project-Id
|
||||
CROWDIN_BASE_PATH=/app/src
|
||||
@@ -0,0 +1,11 @@
|
||||
# Postgresql db container configuration
|
||||
POSTGRES_DB=drive
|
||||
POSTGRES_USER=dinum
|
||||
POSTGRES_PASSWORD=pass
|
||||
|
||||
# App database configuration
|
||||
DB_HOST=postgresql
|
||||
DB_NAME=drive
|
||||
DB_USER=dinum
|
||||
DB_PASSWORD=pass
|
||||
DB_PORT=5432
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Gitlint extra rule to validate that the message title is of the form
|
||||
"<gitmoji>(<scope>) <subject>"
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
|
||||
|
||||
|
||||
class GitmojiTitle(LineRule):
|
||||
"""
|
||||
This rule will enforce that each commit title is of the form "<gitmoji>(<scope>) <subject>"
|
||||
where gitmoji is an emoji from the list defined in https://gitmoji.carloscuesta.me and
|
||||
subject should be all lowercase
|
||||
"""
|
||||
|
||||
id = "UC1"
|
||||
name = "title-should-have-gitmoji-and-scope"
|
||||
target = CommitMessageTitle
|
||||
|
||||
def validate(self, title, _commit):
|
||||
"""
|
||||
Download the list possible gitmojis from the project's github repository and check that
|
||||
title contains one of them.
|
||||
"""
|
||||
gitmojis = requests.get(
|
||||
"https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json"
|
||||
).json()["gitmojis"]
|
||||
emojis = [item["emoji"] for item in gitmojis]
|
||||
pattern = r"^({:s})\(.*\)\s[a-z].*$".format("|".join(emojis))
|
||||
if not re.search(pattern, title):
|
||||
violation_msg = 'Title does not match regex "<gitmoji>(<scope>) <subject>"'
|
||||
return [RuleViolation(self.id, violation_msg, title)]
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": ["github>numerique-gouv/renovate-configuration"],
|
||||
"dependencyDashboard": true,
|
||||
"labels": ["dependencies", "noChangeLog"],
|
||||
"packageRules": [
|
||||
{
|
||||
"enabled": false,
|
||||
"groupName": "ignored python dependencies",
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": []
|
||||
}
|
||||
]
|
||||
}
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
mkdir -p "$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/"
|
||||
PRE_COMMIT_FILE="$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/pre-commit"
|
||||
|
||||
cat <<'EOF' >$PRE_COMMIT_FILE
|
||||
#!/bin/bash
|
||||
|
||||
# directories containing potential secrets
|
||||
DIRS="."
|
||||
|
||||
bold=$(tput bold)
|
||||
normal=$(tput sgr0)
|
||||
|
||||
# allow to read user input, assigns stdin to keyboard
|
||||
exec </dev/tty
|
||||
|
||||
for d in $DIRS; do
|
||||
# find files containing secrets that should be encrypted
|
||||
for f in $(find "${d}" -type f -regex ".*\.enc\..*"); do
|
||||
if ! $(grep -q "unencrypted_suffix" $f); then
|
||||
printf '\xF0\x9F\x92\xA5 '
|
||||
echo "File $f has non encrypted secrets!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
EOF
|
||||
|
||||
chmod +x $PRE_COMMIT_FILE
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
git submodule update --init --recursive
|
||||
git submodule foreach 'git fetch origin; git checkout $(git rev-parse --abbrev-ref HEAD); git reset --hard origin/$(git rev-parse --abbrev-ref HEAD); git submodule update --recursive; git clean -dfx'
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
find . -name "*.enc.*" -exec sops updatekeys -y {} \;
|
||||
@@ -0,0 +1,472 @@
|
||||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=migrations
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=0
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=pylint_django,pylint.extensions.no_self_use
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
disable=bad-inline-option,
|
||||
deprecated-pragma,
|
||||
django-not-configured,
|
||||
file-ignored,
|
||||
locally-disabled,
|
||||
no-self-use,
|
||||
raw-checker-failed,
|
||||
suppressed-message,
|
||||
useless-suppression
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio).You can also give a reporter class, eg
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=optparse.Values,sys.exit
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local,responses,
|
||||
Template,Contact
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||
# not used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=yes
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
# First implementations of CMS wizards have common fields we do not want to factorize for now
|
||||
min-similarity-lines=35
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Naming style matching correct class attribute names
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class names
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-style
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|urlpatterns|logger)$
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
cm,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style
|
||||
method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$
|
||||
|
||||
# Naming style matching correct module names
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in a if statement
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=0
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=builtins.Exception
|
||||
@@ -0,0 +1,3 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
recursive-include src/backend/drive *.html *.png *.gif *.css *.ico *.jpg *.jpeg *.po *.mo *.eot *.svg *.ttf *.woff *.woff2
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Admin config for drive's core app"""
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Service
|
||||
|
||||
|
||||
@admin.register(Service)
|
||||
class ServiceAdmin(admin.ModelAdmin):
|
||||
"""Register the serivce model for the admin site"""
|
||||
|
||||
list_display = ("name", "created_at", "is_active")
|
||||
search_fields = ("name",)
|
||||
list_filter = ("is_active", "created_at")
|
||||
ordering = ("-created_at",)
|
||||
readonly_fields = ("created_at", "token")
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Impress core API endpoints"""
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from rest_framework import exceptions as drf_exceptions
|
||||
from rest_framework import status
|
||||
from rest_framework import views as drf_views
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def exception_handler(exc, context):
|
||||
"""Handle Django ValidationError as an accepted exception.
|
||||
|
||||
For the parameters, see ``exception_handler``
|
||||
This code comes from twidi's gist:
|
||||
https://gist.github.com/twidi/9d55486c36b6a51bdcb05ce3a763e79f
|
||||
"""
|
||||
if isinstance(exc, ValidationError):
|
||||
detail = exc.message_dict
|
||||
|
||||
if hasattr(exc, "message"):
|
||||
detail = exc.message
|
||||
elif hasattr(exc, "messages"):
|
||||
detail = exc.messages
|
||||
exc = drf_exceptions.ValidationError(detail=detail)
|
||||
|
||||
elif isinstance(exc, PydanticValidationError):
|
||||
return Response(
|
||||
[
|
||||
{key: error[key] for key in ("msg", "type", "loc")}
|
||||
for error in exc.errors()
|
||||
],
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
return drf_views.exception_handler(exc, context)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Token authentication."""
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
from rest_framework import authentication, exceptions
|
||||
|
||||
from .models import Service
|
||||
|
||||
|
||||
class ServiceTokenAuthentication(authentication.BaseAuthentication):
|
||||
"""A custom authentication looking for valid tokens among registered services"""
|
||||
|
||||
model = Service
|
||||
|
||||
def authenticate(self, request):
|
||||
"""Authenticate token from the "Authorization" header."""
|
||||
token = request.headers.get("Authorization")
|
||||
if not token:
|
||||
raise exceptions.NotAuthenticated()
|
||||
|
||||
token = token.split(" ")[-1] # Extract token if prefixed with "Token"
|
||||
|
||||
return self.authenticate_credentials(token)
|
||||
|
||||
def authenticate_credentials(self, token):
|
||||
"""Check that the token is registered and valid."""
|
||||
try:
|
||||
service_name = (
|
||||
self.model.objects.only("name").get(token=token, is_active=True).name
|
||||
)
|
||||
except self.model.DoesNotExist as excpt:
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from excpt
|
||||
|
||||
# We don't associate tokens with a user
|
||||
return AnonymousUser(), service_name
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Enums for drive's core app."""
|
||||
|
||||
RELEVANCE = "relevance"
|
||||
|
||||
CREATED_AT = "created_at"
|
||||
IS_PUBLIC = "is_public"
|
||||
SIZE = "size"
|
||||
TITLE = "title"
|
||||
UPDATED_AT = "updated_at"
|
||||
|
||||
ORDER_BY_OPTIONS = (RELEVANCE, TITLE, CREATED_AT, UPDATED_AT, SIZE, IS_PUBLIC)
|
||||
SOURCE_FIELDS = (TITLE, SIZE, CREATED_AT, UPDATED_AT, IS_PUBLIC)
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Factories for the drive's core app"""
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
import factory
|
||||
from faker import Faker
|
||||
|
||||
from .models import Service
|
||||
|
||||
fake = Faker()
|
||||
|
||||
|
||||
class DocumentSchemaFactory(factory.DictFactory):
|
||||
"""
|
||||
A factory for generating dictionaries that represent a document for
|
||||
indexation for testing and development purposes.
|
||||
"""
|
||||
|
||||
id = factory.LazyFunction(uuid4)
|
||||
title = factory.Sequence(lambda n: f"Test title {n!s}")
|
||||
content = factory.Sequence(lambda n: f"Test content {n!s}")
|
||||
created_at = factory.LazyFunction(
|
||||
lambda: fake.date_time_this_decade(tzinfo=timezone.get_current_timezone())
|
||||
)
|
||||
size = factory.LazyFunction(lambda: fake.random_int(min=0, max=1024**2))
|
||||
users = factory.LazyFunction(lambda: [uuid4() for _ in range(3)])
|
||||
groups = factory.LazyFunction(lambda: [slugify(fake.word()) for _ in range(3)])
|
||||
is_public = factory.Faker("boolean")
|
||||
|
||||
@factory.lazy_attribute
|
||||
def updated_at(self):
|
||||
"""Ensure updated_at is after created_at and before now"""
|
||||
return fake.date_time_between(
|
||||
start_date=self.created_at,
|
||||
end_date=timezone.now(),
|
||||
tzinfo=timezone.get_current_timezone(),
|
||||
)
|
||||
|
||||
|
||||
class ServiceFactory(factory.django.DjangoModelFactory):
|
||||
"""
|
||||
A factory for generating service instances for testing and development purposes.
|
||||
"""
|
||||
|
||||
name = factory.Sequence(lambda n: f"test-index-{n!s}")
|
||||
created_at = factory.Faker("date_time_this_year", tzinfo=None)
|
||||
is_active = True
|
||||
|
||||
class Meta:
|
||||
model = Service
|
||||
@@ -0,0 +1,64 @@
|
||||
# Generated by Django 5.0.7 on 2024-09-01 05:31
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Service',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.SlugField(max_length=20, unique=True)),
|
||||
('token', models.CharField(max_length=50)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'service',
|
||||
'verbose_name_plural': 'services',
|
||||
'db_table': 'drive_service',
|
||||
'ordering': ['-is_active', '-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='service',
|
||||
constraint=models.CheckConstraint(check=models.Q(('token__length', 50)), name='token_length_exact_50'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Models for drive's core app"""
|
||||
import secrets
|
||||
import string
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.db.models.functions import Length
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
models.CharField.register_lookup(Length)
|
||||
TOKEN_LENGTH = 50
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""User for the drive application"""
|
||||
|
||||
|
||||
class Service(models.Model):
|
||||
"""Service registered to index its documents to our drive"""
|
||||
|
||||
name = models.SlugField(max_length=20, unique=True)
|
||||
token = models.CharField(max_length=TOKEN_LENGTH)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "drive_service"
|
||||
verbose_name = _("service")
|
||||
verbose_name_plural = _("services")
|
||||
ordering = ["-is_active", "-created_at"]
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
check=models.Q(token__length=TOKEN_LENGTH), name="token_length_exact_50"
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Automatically slugify the service name and generate a token on creation"""
|
||||
self.name = slugify(self.name)
|
||||
if not self.token:
|
||||
self.token = self.generate_secure_token()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def generate_secure_token():
|
||||
"""Generate a secure token with with Python secret module"""
|
||||
characters = string.ascii_letters + string.digits + string.punctuation
|
||||
token = "".join(secrets.choice(characters) for _ in range(TOKEN_LENGTH))
|
||||
return token
|
||||
|
||||
@property
|
||||
def index_name(self):
|
||||
"""Compute index name from service name"""
|
||||
return f"drive-{self.name:s}"
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Opensearch related utils."""
|
||||
from django.conf import settings
|
||||
|
||||
from opensearchpy import OpenSearch
|
||||
from opensearchpy.exceptions import NotFoundError
|
||||
|
||||
client = OpenSearch(
|
||||
hosts=[{"host": settings.OPENSEARCH_HOST, "port": settings.OPENSEARCH_PORT}],
|
||||
http_auth=(settings.OPENSEARCH_USER, settings.OPENSEARCH_PASSWORD),
|
||||
timeout=50,
|
||||
use_ssl=settings.OPENSEARCH_USE_SSL,
|
||||
verify_certs=False,
|
||||
)
|
||||
|
||||
|
||||
def ensure_index_exists(index_name):
|
||||
"""Create index if it does not exist"""
|
||||
try:
|
||||
client.indices.get(index=index_name)
|
||||
except NotFoundError:
|
||||
client.indices.create(
|
||||
index=index_name,
|
||||
body={
|
||||
"mappings": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "keyword", # Primary field for exact matches and sorting
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "text" # Sub-field for full-text search
|
||||
}
|
||||
},
|
||||
},
|
||||
"content": {"type": "text"},
|
||||
"created_at": {"type": "date"},
|
||||
"updated_at": {"type": "date"},
|
||||
"size": {"type": "long"},
|
||||
"users": {"type": "keyword"},
|
||||
"groups": {"type": "keyword"},
|
||||
"is_public": {"type": "boolean"},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Permission classes for drive's core app"""
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class IsAuthAuthenticated(permissions.BasePermission):
|
||||
"""
|
||||
Allows access only to auth authenticated users.
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return bool(request.auth)
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Pydantic model to validate documents before indexation."""
|
||||
from typing import Annotated, List, Literal, Optional
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
from pydantic import (
|
||||
UUID4,
|
||||
AwareDatetime,
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
conint,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from . import enums
|
||||
|
||||
|
||||
class DocumentSchema(BaseModel):
|
||||
"""Schema for validating the documents submitted to our API for indexing"""
|
||||
|
||||
id: UUID4
|
||||
title: Annotated[str, Field(max_length=300)]
|
||||
content: str
|
||||
created_at: AwareDatetime
|
||||
updated_at: AwareDatetime
|
||||
size: Annotated[int, Field(ge=0, le=100 * 1024**3)] # File size limited to 100GB
|
||||
users: List[UUID4] = Field(default_factory=list)
|
||||
groups: List[Annotated[str, Field(pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$")]] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
is_public: bool = Field(default=False)
|
||||
|
||||
model_config = ConfigDict(
|
||||
str_min_length=1, str_strip_whitespace=True, use_enum_values=True
|
||||
)
|
||||
|
||||
@field_validator("title")
|
||||
@staticmethod
|
||||
def normalize_title(value):
|
||||
"""Normalize the title field by stripping whitespace and converting to lowercase"""
|
||||
return value.strip().lower()
|
||||
|
||||
@field_validator("created_at", "updated_at")
|
||||
@staticmethod
|
||||
def must_be_past(value, info):
|
||||
"""Validate that `created_at` and `updated_at` fields are in the past"""
|
||||
if value >= timezone.now():
|
||||
raise ValueError(f"{info.field_name} must be earlier than now")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_update_at_after_created_at(self):
|
||||
"""Date and time of last modification should be later than date and time of creation"""
|
||||
if self.created_at > self.updated_at:
|
||||
raise ValueError("updated_at must be later than created_at")
|
||||
return self
|
||||
|
||||
@field_validator("groups")
|
||||
@staticmethod
|
||||
def validate_groups(groups):
|
||||
"""Validate that group slugs are properly formatted as lowercase and hyphen-separated"""
|
||||
validated_groups = []
|
||||
for value in groups:
|
||||
slug = slugify(value)
|
||||
if value != slug:
|
||||
raise ValueError(
|
||||
f"Groups must be slugs (lowercase, hyphen-separated): {slug:s}"
|
||||
)
|
||||
validated_groups.append(value)
|
||||
return validated_groups
|
||||
|
||||
|
||||
class SearchQueryParametersSchema(BaseModel):
|
||||
"""Schema for validating the querystring on the search API endpoint"""
|
||||
|
||||
q: str
|
||||
is_public: Optional[bool] = None
|
||||
order_by: Optional[Literal[enums.ORDER_BY_OPTIONS]] = Field(default="relevance")
|
||||
order_direction: Optional[Literal["asc", "desc"]] = Field(default="desc")
|
||||
page_number: Optional[conint(ge=1)] = Field(default=1)
|
||||
page_size: Optional[conint(ge=1, le=100)] = Field(default=50)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@staticmethod
|
||||
def handle_lists(values):
|
||||
"""Make sure we get strings and ignore multiple values."""
|
||||
for key, value in values.items():
|
||||
if isinstance(value, list):
|
||||
# Take the first item if it's a list
|
||||
values[key] = value[0] if value else None
|
||||
return values
|
||||
@@ -0,0 +1,290 @@
|
||||
"""Tests indexing documents in OpenSearch over the API"""
|
||||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, opensearch
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_documents_index_bulk_anonymous():
|
||||
"""Anonymous requests should not be allowed to index documents in bulk."""
|
||||
documents = factories.DocumentSchemaFactory.build_batch(3)
|
||||
|
||||
response = APIClient().post("/api/v1.0/documents/", documents, format="json")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_index_bulk_invalid_token():
|
||||
"""Requests with invalid tokens should not be allowed to index documents in bulk."""
|
||||
documents = factories.DocumentSchemaFactory.build_batch(3)
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
documents,
|
||||
HTTP_AUTHORIZATION="Bearer invalid",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid token."}
|
||||
|
||||
|
||||
def test_api_documents_index_bulk_success():
|
||||
"""A registered service should be able to index documents in bulk with a valid token."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(3)
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
documents,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 207
|
||||
responses = response.json()
|
||||
assert len(responses) == 3
|
||||
for result in response.json():
|
||||
assert result["status"] == "success"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field, invalid_value, error_type, error_message",
|
||||
[
|
||||
(
|
||||
"id",
|
||||
"0f9b1c9d-030f-427a-8a0e-6b7c202c5daz", # invalid UUID b/c contains a z
|
||||
"uuid_parsing",
|
||||
(
|
||||
"Input should be a valid UUID, invalid character: expected an optional "
|
||||
"prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `z` at 36"
|
||||
),
|
||||
),
|
||||
("title", 1, "string_type", "Input should be a valid string"),
|
||||
(
|
||||
"title",
|
||||
"a" * 301,
|
||||
"string_too_long",
|
||||
"String should have at most 300 characters",
|
||||
),
|
||||
("content", 1, "string_type", "Input should be a valid string"),
|
||||
(
|
||||
"created_at",
|
||||
"invalid_date",
|
||||
"datetime_from_date_parsing",
|
||||
"Input should be a valid datetime or date, invalid character in year",
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
"invalid_date",
|
||||
"datetime_from_date_parsing",
|
||||
"Input should be a valid datetime or date, invalid character in year",
|
||||
),
|
||||
(
|
||||
"size",
|
||||
64448894017.3,
|
||||
"int_from_float",
|
||||
"Input should be a valid integer, got a number with a fractional part",
|
||||
),
|
||||
(
|
||||
"size",
|
||||
"not an integer",
|
||||
"int_parsing",
|
||||
"Input should be a valid integer, unable to parse string as an integer",
|
||||
),
|
||||
(
|
||||
"users",
|
||||
"33052c8b-3181-4420-aede-f8396fc0f9a1",
|
||||
"list_type",
|
||||
"Input should be a valid list",
|
||||
),
|
||||
(
|
||||
"users",
|
||||
["33052c8b-3181-4420-aede-f8396fc0f9az"], # invalid UUID b/c contains a z
|
||||
"uuid_parsing",
|
||||
(
|
||||
"Input should be a valid UUID, invalid character: expected an optional "
|
||||
"prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `z` at 36"
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
["not_a_slug"],
|
||||
"string_pattern_mismatch",
|
||||
"String should match pattern '^[a-z0-9]+(?:-[a-z0-9]+)*$'",
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
"not-a-list",
|
||||
"list_type",
|
||||
"Input should be a valid list",
|
||||
),
|
||||
(
|
||||
"is_public",
|
||||
"invalid_boolean",
|
||||
"bool_parsing",
|
||||
"Input should be a valid boolean, unable to interpret input",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_api_documents_index_bulk_invalid_document(
|
||||
field, invalid_value, error_type, error_message
|
||||
):
|
||||
"""Test bulk document indexing with various invalid fields."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(3)
|
||||
|
||||
# Modify the first document with the invalid value for the specified field
|
||||
documents[0][field] = invalid_value
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
documents,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
responses = response.json()
|
||||
|
||||
assert responses[0]["status"] == "error"
|
||||
assert len(responses[0]["errors"]) == 1
|
||||
assert responses[0]["errors"][0]["msg"] == error_message
|
||||
assert responses[0]["errors"][0]["type"] == error_type
|
||||
|
||||
for i in [1, 2]:
|
||||
assert responses[i]["status"] == "valid"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field", ["id", "title", "content", "size", "created_at", "updated_at"]
|
||||
)
|
||||
def test_api_documents_index_bulk_required(field):
|
||||
"""Test bulk document indexing with a required field missing."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(3)
|
||||
|
||||
del documents[0][field]
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
documents,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
responses = response.json()
|
||||
assert responses[0]["status"] == "error"
|
||||
assert len(responses[0]["errors"]) == 1
|
||||
assert responses[0]["errors"][0]["msg"] == "Field required"
|
||||
assert responses[0]["errors"][0]["type"] == "missing"
|
||||
|
||||
for i in [1, 2]:
|
||||
assert responses[i]["status"] == "valid"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field,default_value",
|
||||
[
|
||||
("users", []),
|
||||
("groups", []),
|
||||
("is_public", False),
|
||||
],
|
||||
)
|
||||
def test_api_documents_index_bulk_default(field, default_value):
|
||||
"""Test bulk document indexing while removing optional fields that have default values."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(3)
|
||||
|
||||
del documents[0][field]
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
documents,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 207
|
||||
responses = response.json()
|
||||
assert len(responses) == 3
|
||||
for result in response.json():
|
||||
assert result["status"] == "success"
|
||||
|
||||
indexed_document = opensearch.client.get(
|
||||
index=service.index_name, id=responses[0]["_id"]
|
||||
)["_source"]
|
||||
assert indexed_document[field] == default_value
|
||||
|
||||
|
||||
def test_api_documents_index_bulk_updated_at_before_created_at():
|
||||
"""Test bulk document indexing with updated_at before created_at."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(3)
|
||||
|
||||
documents[0]["updated_at"] = documents[0]["created_at"] - datetime.timedelta(
|
||||
seconds=1
|
||||
)
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
documents,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
responses = response.json()
|
||||
assert responses[0]["status"] == "error"
|
||||
assert len(responses[0]["errors"]) == 1
|
||||
assert (
|
||||
responses[0]["errors"][0]["msg"]
|
||||
== "Value error, updated_at must be later than created_at"
|
||||
)
|
||||
assert responses[0]["errors"][0]["type"] == "value_error"
|
||||
|
||||
for i in [1, 2]:
|
||||
assert responses[i]["status"] == "valid"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field",
|
||||
["created_at", "updated_at"],
|
||||
)
|
||||
def test_api_documents_index_bulk_datetime_future(field):
|
||||
"""Test bulk document indexing with datetimes in the future."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(3)
|
||||
|
||||
now = timezone.now()
|
||||
documents[0][field] = now + datetime.timedelta(seconds=3)
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
documents,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
responses = response.json()
|
||||
assert responses[0]["status"] == "error"
|
||||
assert len(responses[0]["errors"]) == 1
|
||||
assert (
|
||||
responses[0]["errors"][0]["msg"]
|
||||
== f"Value error, {field:s} must be earlier than now"
|
||||
)
|
||||
assert responses[0]["errors"][0]["type"] == "value_error"
|
||||
|
||||
for i in [1, 2]:
|
||||
assert responses[i]["status"] == "valid"
|
||||
@@ -0,0 +1,254 @@
|
||||
"""Tests indexing documents in OpenSearch over the API"""
|
||||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, opensearch
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_documents_index_single_anonymous():
|
||||
"""Anonymous requests should not be allowed to index documents."""
|
||||
document = factories.DocumentSchemaFactory.build()
|
||||
|
||||
response = APIClient().post("/api/v1.0/documents/", document, format="json")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_index_single_invalid_token():
|
||||
"""Requests with invalid tokens should not be allowed to index documents."""
|
||||
document = factories.DocumentSchemaFactory.build()
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
document,
|
||||
HTTP_AUTHORIZATION="Bearer invalid",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid token."}
|
||||
|
||||
|
||||
def test_api_documents_index_single_success():
|
||||
"""A registered service should be able to index document with a valid token."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
document = factories.DocumentSchemaFactory.build()
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
document,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json()["_id"] == str(document["id"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field, invalid_value, error_type, error_message",
|
||||
[
|
||||
(
|
||||
"id",
|
||||
"0f9b1c9d-030f-427a-8a0e-6b7c202c5daz", # invalid UUID b/c contains a z*
|
||||
"uuid_parsing",
|
||||
(
|
||||
"Input should be a valid UUID, invalid character: expected an optional "
|
||||
"prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `z` at 36"
|
||||
),
|
||||
),
|
||||
("title", 1, "string_type", "Input should be a valid string"),
|
||||
(
|
||||
"title",
|
||||
"a" * 301,
|
||||
"string_too_long",
|
||||
"String should have at most 300 characters",
|
||||
),
|
||||
("content", 1, "string_type", "Input should be a valid string"),
|
||||
(
|
||||
"created_at",
|
||||
"invalid_date",
|
||||
"datetime_from_date_parsing",
|
||||
"Input should be a valid datetime or date, invalid character in year",
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
"invalid_date",
|
||||
"datetime_from_date_parsing",
|
||||
"Input should be a valid datetime or date, invalid character in year",
|
||||
),
|
||||
(
|
||||
"size",
|
||||
64448894017.3,
|
||||
"int_from_float",
|
||||
"Input should be a valid integer, got a number with a fractional part",
|
||||
),
|
||||
(
|
||||
"size",
|
||||
"not an integer",
|
||||
"int_parsing",
|
||||
"Input should be a valid integer, unable to parse string as an integer",
|
||||
),
|
||||
(
|
||||
"users",
|
||||
"33052c8b-3181-4420-aede-f8396fc0f9a1",
|
||||
"list_type",
|
||||
("Input should be a valid list"),
|
||||
),
|
||||
(
|
||||
"users",
|
||||
["33052c8b-3181-4420-aede-f8396fc0f9az"], # invalid UUID b/c contains a z*
|
||||
"uuid_parsing",
|
||||
(
|
||||
"Input should be a valid UUID, invalid character: expected an optional "
|
||||
"prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `z` at 36"
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
["not_a_slug"],
|
||||
"string_pattern_mismatch",
|
||||
"String should match pattern '^[a-z0-9]+(?:-[a-z0-9]+)*$'",
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
"not-a-list",
|
||||
"list_type",
|
||||
("Input should be a valid list"),
|
||||
),
|
||||
(
|
||||
"is_public",
|
||||
"invalid_boolean",
|
||||
"bool_parsing",
|
||||
"Input should be a valid boolean, unable to interpret input",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_api_documents_index_single_invalid_document(
|
||||
field, invalid_value, error_type, error_message
|
||||
):
|
||||
"""Test document indexing with various invalid fields."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
document = factories.DocumentSchemaFactory.build()
|
||||
|
||||
# Modify the document with the invalid value for the specified field
|
||||
document[field] = invalid_value
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
document,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.data[0]["msg"] == error_message
|
||||
assert response.data[0]["type"] == error_type
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field", ["id", "title", "content", "size", "created_at", "updated_at"]
|
||||
)
|
||||
def test_api_documents_index_single_required(field):
|
||||
"""Test document indexing with a required field missing."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
document = factories.DocumentSchemaFactory.build()
|
||||
|
||||
del document[field]
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
document,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.data[0]["msg"] == "Field required"
|
||||
assert response.data[0]["type"] == "missing"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field,default_value",
|
||||
[
|
||||
("users", []),
|
||||
("groups", []),
|
||||
("is_public", False),
|
||||
],
|
||||
)
|
||||
def test_api_documents_index_single_default(field, default_value):
|
||||
"""Test document indexing while removing optional fields that have default values."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
document = factories.DocumentSchemaFactory.build()
|
||||
|
||||
del document[field]
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
document,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json()["_id"] == str(document["id"])
|
||||
|
||||
indexed_document = opensearch.client.get(
|
||||
index=service.index_name, id=str(document["id"])
|
||||
)["_source"]
|
||||
assert indexed_document[field] == default_value
|
||||
|
||||
|
||||
def test_api_documents_index_single_udpated_at_before_created():
|
||||
"""Test document indexing with updated_at before created_at."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
document = factories.DocumentSchemaFactory.build()
|
||||
|
||||
document["updated_at"] = document["created_at"] - datetime.timedelta(seconds=1)
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
document,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert (
|
||||
response.data[0]["msg"]
|
||||
== "Value error, updated_at must be later than created_at"
|
||||
)
|
||||
assert response.data[0]["type"] == "value_error"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field",
|
||||
["created_at", "updated_at"],
|
||||
)
|
||||
def test_api_documents_index_single_datetime_future(field):
|
||||
"""Test document indexing with datetimes in the future."""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
document = factories.DocumentSchemaFactory.build()
|
||||
|
||||
now = timezone.now()
|
||||
document[field] = now + datetime.timedelta(seconds=3)
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
document,
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.data[0]["msg"] == f"Value error, {field:s} must be earlier than now"
|
||||
assert response.data[0]["type"] == "value_error"
|
||||
@@ -0,0 +1,448 @@
|
||||
"""
|
||||
Test suite for searching documents in OpenSearch over the API.
|
||||
|
||||
Don't use pytest parametrized tests because batch generation and indexing
|
||||
of documents is slow and better done only once.
|
||||
"""
|
||||
import operator
|
||||
|
||||
import pytest
|
||||
from opensearchpy.helpers import bulk
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import enums, factories, opensearch
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def prepare_index(index_name, documents):
|
||||
"""Prepare the search index before testing a query on it."""
|
||||
|
||||
documents = documents if isinstance(documents, list) else [documents]
|
||||
|
||||
# Ensure existence of an empty index
|
||||
opensearch.client.indices.delete(index=index_name)
|
||||
opensearch.ensure_index_exists(index_name)
|
||||
|
||||
# Bulk index documents
|
||||
actions = []
|
||||
for document in documents:
|
||||
_source = document.copy()
|
||||
_id = _source.pop("id")
|
||||
actions.append(
|
||||
{
|
||||
"_op_type": "index",
|
||||
"_index": index_name,
|
||||
"_id": _id,
|
||||
"_source": _source,
|
||||
}
|
||||
)
|
||||
bulk(opensearch.client, actions)
|
||||
|
||||
opensearch.client.indices.refresh(index=index_name)
|
||||
assert opensearch.client.count(index=index_name)["count"] == len(documents)
|
||||
|
||||
|
||||
def test_api_documents_search_query_title():
|
||||
"""Searching a document by its title should work as expected"""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
document = factories.DocumentSchemaFactory.build(
|
||||
title="The quick brown fox", content="the wolf"
|
||||
)
|
||||
|
||||
# Add other documents
|
||||
other_fox_document = factories.DocumentSchemaFactory.build(
|
||||
title="The blue fox", content="the wolf"
|
||||
)
|
||||
no_fox_document = factories.DocumentSchemaFactory.build(
|
||||
title="The brown goat", content="the wolf"
|
||||
)
|
||||
documents = [document, other_fox_document, no_fox_document]
|
||||
prepare_index(service.index_name, documents)
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/",
|
||||
{"q": "a quick fox"},
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 2
|
||||
|
||||
fox_data = response.json()[0]
|
||||
assert list(fox_data.keys()) == ["_index", "_id", "_score", "_source", "fields"]
|
||||
assert fox_data["_id"] == str(document["id"])
|
||||
assert fox_data["_source"] == {
|
||||
"size": document["size"],
|
||||
"created_at": document["created_at"].isoformat(),
|
||||
"updated_at": document["updated_at"].isoformat(),
|
||||
"is_public": document["is_public"],
|
||||
"title": "The quick brown fox",
|
||||
}
|
||||
assert fox_data["fields"] == {"number_of_users": [3], "number_of_groups": [3]}
|
||||
|
||||
other_fox_data = response.json()[1]
|
||||
assert list(other_fox_data.keys()) == [
|
||||
"_index",
|
||||
"_id",
|
||||
"_score",
|
||||
"_source",
|
||||
"fields",
|
||||
]
|
||||
assert other_fox_data["_id"] == str(other_fox_document["id"])
|
||||
assert other_fox_data["_source"] == {
|
||||
"size": other_fox_document["size"],
|
||||
"created_at": other_fox_document["created_at"].isoformat(),
|
||||
"updated_at": other_fox_document["updated_at"].isoformat(),
|
||||
"is_public": other_fox_document["is_public"],
|
||||
"title": "The blue fox",
|
||||
}
|
||||
assert other_fox_data["fields"] == {"number_of_users": [3], "number_of_groups": [3]}
|
||||
|
||||
|
||||
def test_api_documents_search_query_content():
|
||||
"""Searching a document by its content should work as expected"""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
document = factories.DocumentSchemaFactory.build(
|
||||
title="the wolf", content="The quick brown fox"
|
||||
)
|
||||
|
||||
# Add other documents
|
||||
other_fox_document = factories.DocumentSchemaFactory.build(
|
||||
title="the wolf", content="The blue fox"
|
||||
)
|
||||
no_fox_document = factories.DocumentSchemaFactory.build(
|
||||
title="the wolf", content="The brown goat"
|
||||
)
|
||||
prepare_index(service.index_name, [document, other_fox_document, no_fox_document])
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/",
|
||||
{"q": "a quick fox"},
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 2
|
||||
|
||||
fox_data = response.json()[0]
|
||||
assert list(fox_data.keys()) == ["_index", "_id", "_score", "_source", "fields"]
|
||||
assert fox_data["_id"] == str(document["id"])
|
||||
assert fox_data["_source"] == {
|
||||
"size": document["size"],
|
||||
"created_at": document["created_at"].isoformat(),
|
||||
"updated_at": document["updated_at"].isoformat(),
|
||||
"is_public": document["is_public"],
|
||||
"title": document["title"],
|
||||
}
|
||||
assert fox_data["fields"] == {"number_of_users": [3], "number_of_groups": [3]}
|
||||
|
||||
other_fox_data = response.json()[1]
|
||||
assert list(other_fox_data.keys()) == [
|
||||
"_index",
|
||||
"_id",
|
||||
"_score",
|
||||
"_source",
|
||||
"fields",
|
||||
]
|
||||
assert other_fox_data["_id"] == str(other_fox_document["id"])
|
||||
assert other_fox_data["_source"] == {
|
||||
"size": other_fox_document["size"],
|
||||
"created_at": other_fox_document["created_at"].isoformat(),
|
||||
"updated_at": other_fox_document["updated_at"].isoformat(),
|
||||
"is_public": other_fox_document["is_public"],
|
||||
"title": other_fox_document["title"],
|
||||
}
|
||||
assert other_fox_data["fields"] == {"number_of_users": [3], "number_of_groups": [3]}
|
||||
|
||||
|
||||
def test_api_documents_search_ordering_by_fields():
|
||||
"""It should be possible to order by several fields"""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(4)
|
||||
prepare_index(service.index_name, documents)
|
||||
|
||||
parameters = [
|
||||
(enums.TITLE, "asc"),
|
||||
(enums.TITLE, "desc"),
|
||||
(enums.CREATED_AT, "asc"),
|
||||
(enums.CREATED_AT, "desc"),
|
||||
(enums.UPDATED_AT, "asc"),
|
||||
(enums.UPDATED_AT, "desc"),
|
||||
(enums.SIZE, "asc"),
|
||||
(enums.SIZE, "desc"),
|
||||
(enums.IS_PUBLIC, "asc"),
|
||||
(enums.IS_PUBLIC, "desc"),
|
||||
]
|
||||
|
||||
for field, direction in parameters:
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/documents/?q=*&order_by={field}&order_direction={direction}",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
responses = response.json()
|
||||
assert len(responses) == 4
|
||||
|
||||
# Check that results are sorted by the field as expected
|
||||
compare = operator.le if direction == "asc" else operator.ge
|
||||
for i in range(len(responses) - 1):
|
||||
assert compare(
|
||||
responses[i]["_source"][field], responses[i + 1]["_source"][field]
|
||||
)
|
||||
|
||||
|
||||
def test_api_documents_search_ordering_by_relevance():
|
||||
"""It should be possible to order by relevance (score)"""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(4)
|
||||
prepare_index(service.index_name, documents)
|
||||
|
||||
for direction in ["asc", "desc"]:
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/documents/?q=*&order_by=relevance&order_direction={direction}",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
responses = response.json()
|
||||
assert len(responses) == 4
|
||||
|
||||
# Check that results are sorted by score as expected
|
||||
compare = operator.le if direction == "asc" else operator.ge
|
||||
for i in range(len(responses) - 1):
|
||||
assert compare(responses[i]["_score"], responses[i + 1]["_score"])
|
||||
|
||||
|
||||
def test_api_documents_search_ordering_by_unknown_field():
|
||||
"""Trying to sort by an unknown field should return a 400 error"""
|
||||
|
||||
# Setup: Initialize the service and documents only once
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(2)
|
||||
prepare_index(service.index_name, documents)
|
||||
|
||||
# Define the parameters manually
|
||||
directions = ["asc", "desc"]
|
||||
|
||||
# Perform the parameterized tests
|
||||
for direction in directions:
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/documents/?q=*&order_by=unknown&order_direction={direction}",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == [
|
||||
{
|
||||
"loc": ["order_by"],
|
||||
"msg": (
|
||||
"Input should be 'relevance', 'title', 'created_at', "
|
||||
"'updated_at', 'size' or 'is_public'"
|
||||
),
|
||||
"type": "literal_error",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_api_documents_search_ordering_by_unknown_direction():
|
||||
"""Trying to sort with an unknown direction should return a 400 error"""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(2)
|
||||
prepare_index(service.index_name, documents)
|
||||
|
||||
for field in enums.ORDER_BY_OPTIONS:
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/documents/?q=*&order_by={field}&order_direction=unknown",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == [
|
||||
{
|
||||
"loc": ["order_direction"],
|
||||
"msg": "Input should be 'asc' or 'desc'",
|
||||
"type": "literal_error",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_api_documents_search_filtering_by_is_public():
|
||||
"""It should be possible to filter results by their publication status"""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(4)
|
||||
prepare_index(service.index_name, documents)
|
||||
|
||||
parameters = [
|
||||
[True, "True"],
|
||||
[False, "False"],
|
||||
[True, "true"],
|
||||
[False, "false"],
|
||||
[True, "1"],
|
||||
[False, "0"],
|
||||
]
|
||||
|
||||
for public_boolean, public_string in parameters:
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/documents/?q=*&is_public={public_string}",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
responses = response.json()
|
||||
|
||||
for result in responses:
|
||||
assert public_boolean == result["_source"]["is_public"]
|
||||
|
||||
|
||||
# Pagination
|
||||
|
||||
|
||||
def test_api_documents_search_pagination_basic():
|
||||
"""Pagination should correctly return documents for the specified page and page size"""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(9)
|
||||
ids = [str(doc["id"]) for doc in documents]
|
||||
prepare_index(service.index_name, documents)
|
||||
|
||||
# Request the first page with a page size of 3
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?q=*&page_number=1&page_size=3",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
responses = response.json()
|
||||
assert len(responses) == 3 # Page size is 3
|
||||
assert [r["_id"] for r in responses] == ids[0:3]
|
||||
|
||||
# Request the second page with a page size of 3
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?q=*&page_number=2&page_size=3",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
responses = response.json()
|
||||
assert len(responses) == 3
|
||||
assert [r["_id"] for r in responses] == ids[3:6]
|
||||
|
||||
# Request the third page with a page size of 5 (should contain the remaining 3 documents)
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?q=*&page_number=3&page_size=3",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
responses = response.json()
|
||||
assert len(responses) == 3
|
||||
assert [r["_id"] for r in responses] == ids[6:9]
|
||||
|
||||
|
||||
def test_api_documents_search_pagination_last_page_edge_case():
|
||||
"""Requesting the last page should return the correct number of remaining documents"""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(8)
|
||||
ids = [str(doc["id"]) for doc in documents]
|
||||
prepare_index(service.index_name, documents)
|
||||
|
||||
# Request the first page with a page size of 3
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?q=*&page_number=1&page_size=3",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 3
|
||||
assert [r["_id"] for r in response.json()] == ids[0:3]
|
||||
|
||||
# Request the third page with a page size of 3 (should contain the last 1 document)
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?q=*&page_number=3&page_size=3",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 2 # Only 2 documents should be on the last page
|
||||
assert [r["_id"] for r in response.json()] == ids[6:]
|
||||
|
||||
|
||||
def test_api_documents_search_pagination_out_of_bounds():
|
||||
"""
|
||||
Requesting a page number that exceeds the total number of pages should return an empty list
|
||||
"""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(4)
|
||||
prepare_index(service.index_name, documents)
|
||||
|
||||
# Request the fourth page with a page size of 2 (there are only 2 pages)
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?q=*&page_number=4&page_size=2",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 0 # No documents should be returned
|
||||
|
||||
|
||||
def test_api_documents_search_pagination_invalid_parameters():
|
||||
"""Invalid pagination parameters should result in a 400 error"""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
documents = factories.DocumentSchemaFactory.build_batch(4)
|
||||
prepare_index(service.index_name, documents)
|
||||
|
||||
parameters = [
|
||||
(
|
||||
"invalid",
|
||||
10,
|
||||
"int_parsing",
|
||||
"Input should be a valid integer, unable to parse string as an integer",
|
||||
),
|
||||
(
|
||||
1,
|
||||
"invalid",
|
||||
"int_parsing",
|
||||
"Input should be a valid integer, unable to parse string as an integer",
|
||||
),
|
||||
(-1, 10, "greater_than_equal", "Input should be greater than or equal to 1"),
|
||||
(1, -10, "greater_than_equal", "Input should be greater than or equal to 1"),
|
||||
(0, 10, "greater_than_equal", "Input should be greater than or equal to 1"),
|
||||
(1, 0, "greater_than_equal", "Input should be greater than or equal to 1"),
|
||||
]
|
||||
|
||||
for page_number, page_size, error_type, error_message in parameters:
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/documents/?q=*&page_number={page_number}&page_size={page_size}",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.data[0]["msg"] == error_message
|
||||
assert response.data[0]["type"] == error_type
|
||||
|
||||
|
||||
def test_api_documents_search_pagination_with_filtering():
|
||||
"""Pagination should work correctly when combined with filtering by is_public"""
|
||||
service = factories.ServiceFactory(name="test-service")
|
||||
public_documents = factories.DocumentSchemaFactory.build_batch(3, is_public=True)
|
||||
public_ids = [str(doc["id"]) for doc in public_documents]
|
||||
private_documents = factories.DocumentSchemaFactory.build_batch(2, is_public=False)
|
||||
prepare_index(service.index_name, public_documents + private_documents)
|
||||
|
||||
# Filter by public documents, request first page
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?q=*&is_public=true&page_number=1&page_size=2",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 2
|
||||
assert [r["_id"] for r in response.json()] == public_ids[0:2]
|
||||
|
||||
# Request second page for public documents (remaining 1 document)
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?q=*&is_public=true&page_number=2&page_size=2",
|
||||
HTTP_AUTHORIZATION=f"Bearer {service.token:s}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 1
|
||||
assert [r["_id"] for r in response.json()] == public_ids[2:]
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Tests Service model for drive's core app."""
|
||||
|
||||
from django.db import DataError, IntegrityError
|
||||
|
||||
import pytest
|
||||
|
||||
from core import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_models_services_name_unique():
|
||||
"""The name field should be unique across services."""
|
||||
service = factories.ServiceFactory()
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
factories.ServiceFactory(name=service.name)
|
||||
|
||||
|
||||
def test_models_services_name_slugified():
|
||||
"""The name field should be slugified."""
|
||||
service = factories.ServiceFactory(name="My service name")
|
||||
assert service.name == "my-service-name"
|
||||
|
||||
|
||||
def test_models_services_index_name():
|
||||
"""The index name should be computed as a property from the service name."""
|
||||
service = factories.ServiceFactory(name="My service name")
|
||||
assert service.index_name == "drive-my-service-name"
|
||||
|
||||
|
||||
def test_models_services_token_50_characters_exact():
|
||||
"""The token field should be 50 characters long."""
|
||||
service = factories.ServiceFactory()
|
||||
assert len(service.token) == 50
|
||||
|
||||
|
||||
def test_models_services_token_50_characters_less():
|
||||
"""The token field should not be less than 50 characters long."""
|
||||
with pytest.raises(IntegrityError):
|
||||
factories.ServiceFactory(token="a" * 49)
|
||||
|
||||
|
||||
def test_models_services_token_50_characters_more():
|
||||
"""The token field should be 50 characters long."""
|
||||
with pytest.raises(DataError):
|
||||
factories.ServiceFactory(token="a" * 51)
|
||||
@@ -0,0 +1,8 @@
|
||||
"""URL configuration for drive's core app."""
|
||||
from django.urls import path
|
||||
|
||||
from .views import DocumentView
|
||||
|
||||
urlpatterns = [
|
||||
path("documents/", DocumentView.as_view(), name="document"),
|
||||
]
|
||||
@@ -0,0 +1,239 @@
|
||||
"""Views for drive's core app."""
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from rest_framework import status, views
|
||||
from rest_framework.response import Response
|
||||
from urllib3.exceptions import ReadTimeoutError
|
||||
|
||||
from . import enums, schemas
|
||||
from .authentication import ServiceTokenAuthentication
|
||||
from .opensearch import client, ensure_index_exists
|
||||
from .permissions import IsAuthAuthenticated
|
||||
|
||||
|
||||
class DocumentView(views.APIView):
|
||||
"""
|
||||
API view for managing documents in an OpenSearch index.
|
||||
|
||||
This view provides functionality for both indexing and searching documents
|
||||
within an OpenSearch index dedicated to the authenticated service. The class
|
||||
supports the following operations:
|
||||
|
||||
1. **Document Indexing (POST)**:
|
||||
- Handles both single document and bulk document indexing.
|
||||
- The index is dynamically determined based on the service authentication token,
|
||||
ensuring that each service has its own isolated index.
|
||||
|
||||
2. **Document Search (GET)**:
|
||||
- Enables searching through indexed documents with support for various filters
|
||||
and sorting options.
|
||||
- The search results can be sorted or filtered via querystring parameters.
|
||||
"""
|
||||
|
||||
authentication_classes = [ServiceTokenAuthentication]
|
||||
permission_classes = [IsAuthAuthenticated]
|
||||
|
||||
@property
|
||||
def index_name(self):
|
||||
"""Compute index name from the service name extracted during authentication"""
|
||||
return f"drive-{self.request.auth}"
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
API view for indexing documents into OpenSearch index of the authenticated service.
|
||||
|
||||
This view supports both single document indexing and bulk indexing. It handles
|
||||
the following scenarios based on the type of request data:
|
||||
|
||||
1. **Single Document Indexing**: If the request contains a single document (as a
|
||||
dictionary), it will be indexed into an OpenSearch index named `drive-{auth_token}`.
|
||||
On success, the indexed document is returned with a `201 Created` status. If an
|
||||
error occurs, a `400 Bad Request` response with an error message is returned.
|
||||
|
||||
2. **Bulk Indexing**: If the request contains a list of documents, each document is
|
||||
validated and indexed in bulk. The response includes a detailed status of each
|
||||
document, indicating whether it was successfully indexed or if an error occurred.
|
||||
The HTTP status code for the bulk indexing operation is `207 Multi-Status`, and the
|
||||
response body contains information about the success or failure of each individual
|
||||
document.
|
||||
|
||||
Methods:
|
||||
-------
|
||||
post(request, *args, **kwargs):
|
||||
Handles POST requests to index either a single document or a list of documents.
|
||||
- **Single Document**: Expects a dictionary representing a document.
|
||||
- **Bulk Indexing**: Expects a list of dictionaries, each representing a document.
|
||||
|
||||
Request Data:
|
||||
-------------
|
||||
For single document indexing:
|
||||
- Content-Type: application/json
|
||||
- Body: JSON object representing a document.
|
||||
|
||||
For bulk indexing:
|
||||
- Content-Type: application/json
|
||||
- Body: JSON array of JSON objects, each representing a document.
|
||||
|
||||
Responses:
|
||||
-----------
|
||||
- **Single Document Indexing**:
|
||||
- 201 Created: Returns the indexed document.
|
||||
- 400 Bad Request: Returns an error message if the document is invalid or if
|
||||
indexing fails.
|
||||
|
||||
- **Bulk Indexing**:
|
||||
- 207 Multi-Status if all documents formatting is correct, 400 Bad Request otherwise:
|
||||
- Returns a list of results for all documents, with details of success and indexing
|
||||
errors.
|
||||
"""
|
||||
|
||||
if isinstance(request.data, list):
|
||||
# Bulk indexing several documents
|
||||
results = []
|
||||
actions = []
|
||||
has_errors = False
|
||||
|
||||
for i, document_data in enumerate(request.data):
|
||||
try:
|
||||
document = schemas.DocumentSchema(**document_data)
|
||||
except PydanticValidationError as excpt:
|
||||
errors = [
|
||||
{key: error[key] for key in ("msg", "type", "loc")}
|
||||
for error in excpt.errors()
|
||||
]
|
||||
results.append({"index": i, "status": "error", "errors": errors})
|
||||
has_errors = True
|
||||
else:
|
||||
document_dict = document.model_dump()
|
||||
_id = document_dict.pop("id")
|
||||
actions.append({"index": {"_id": _id}})
|
||||
actions.append(document_dict)
|
||||
results.append({"index": i, "_id": _id, "status": "valid"})
|
||||
if has_errors:
|
||||
return Response(results, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
ensure_index_exists(self.index_name)
|
||||
response = client.bulk(index=self.index_name, body=actions)
|
||||
for i, item in enumerate(response["items"]):
|
||||
if item["index"]["status"] != 201:
|
||||
results[i]["status"] = "error"
|
||||
results[i]["message"] = (
|
||||
item["index"].get("error", {}).get("reason", "Unknown error")
|
||||
)
|
||||
else:
|
||||
results[i]["status"] = "success"
|
||||
|
||||
return Response(results, status=status.HTTP_207_MULTI_STATUS)
|
||||
|
||||
# Indexing a single document
|
||||
document = schemas.DocumentSchema(**request.data)
|
||||
document_dict = document.model_dump()
|
||||
_id = document_dict.pop("id")
|
||||
try:
|
||||
client.index(index=self.index_name, body=document_dict, id=_id)
|
||||
except ReadTimeoutError:
|
||||
ensure_index_exists(self.index_name)
|
||||
client.index(index=self.index_name, body=document_dict, id=_id)
|
||||
|
||||
return Response(
|
||||
{"status": "created", "_id": _id}, status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handle GET requests to perform a search on indexed documents with optional filtering
|
||||
and ordering.
|
||||
|
||||
The search query should be provided as a query parameter 'q'. The method constructs a
|
||||
search request to OpenSearch using the specified query, with the option to filter by
|
||||
'is_public' and order by 'relevance', 'created_at', 'updated_at', or 'size'.
|
||||
The results are further filtered by 'users' and 'groups' based on the authentication
|
||||
header.
|
||||
|
||||
Query Parameters:
|
||||
---------------
|
||||
q : str
|
||||
The search query string. This is a required parameter.
|
||||
is_public : bool, optional
|
||||
Filter results based on the 'is_public' field.
|
||||
order_by : str, optional
|
||||
Order results by 'relevance', 'created_at', 'updated_at', or 'size'.
|
||||
Defaults to 'relevance' if not specified.
|
||||
order_direction : str, optional
|
||||
Order direction, 'asc' for ascending or 'desc' for descending.
|
||||
Defaults to 'desc'.
|
||||
page_number : int, optional
|
||||
The page number to retrieve.
|
||||
Defaults to 1 if not specified.
|
||||
page_size : int, optional
|
||||
The number of results to return per page.
|
||||
Defaults to 50 if not specified.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
Response : rest_framework.response.Response
|
||||
- 200 OK: Returns a list of search results matching the query.
|
||||
- 400 Bad Request: If the query parameter 'q' is not provided or invalid.
|
||||
"""
|
||||
# Extract and validate query parameters using Pydantic schema
|
||||
params = schemas.SearchQueryParametersSchema(**request.GET)
|
||||
|
||||
# Compute pagination parameters
|
||||
from_value = (params.page_number - 1) * params.page_size
|
||||
size_value = params.page_size
|
||||
|
||||
# Prepare the search query
|
||||
search_body = {
|
||||
"_source": enums.SOURCE_FIELDS, # limit the fields to return
|
||||
"script_fields": {
|
||||
"number_of_users": {"script": {"source": "doc['users'].size()"}},
|
||||
"number_of_groups": {"script": {"source": "doc['groups'].size()"}},
|
||||
},
|
||||
"query": {"bool": {"must": [], "filter": []}},
|
||||
"sort": [],
|
||||
"from": from_value,
|
||||
"size": size_value,
|
||||
}
|
||||
|
||||
# Adding the text query
|
||||
if params.q == "*":
|
||||
search_body["query"]["bool"]["must"].append({"match_all": {}})
|
||||
else:
|
||||
search_body["query"]["bool"]["must"].append(
|
||||
{
|
||||
"multi_match": {
|
||||
"query": params.q,
|
||||
# Give title more importance over content by a power of 3
|
||||
"fields": ["title.text^3", "content"],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Add sorting logic based on relevance or specified field
|
||||
if params.order_by == enums.RELEVANCE:
|
||||
search_body["sort"].append({"_score": {"order": params.order_direction}})
|
||||
else:
|
||||
search_body["sort"].append(
|
||||
{params.order_by: {"order": params.order_direction}}
|
||||
)
|
||||
|
||||
# Filter by is_public if provided
|
||||
if params.is_public is not None:
|
||||
search_body["query"]["bool"]["filter"].append(
|
||||
{"term": {enums.IS_PUBLIC: params.is_public}}
|
||||
)
|
||||
|
||||
# Filter by users and groups based on authentication$
|
||||
# user = request.user
|
||||
# groups = user.get_teams()
|
||||
|
||||
# search_body['query']['bool']['filter'].append({
|
||||
# "terms": {"users": [user.sub]}
|
||||
# })
|
||||
|
||||
# search_body['query']['bool']['filter'].append({
|
||||
# "terms": {"groups": list(groups)}
|
||||
# })
|
||||
|
||||
response = client.search(index=self.index_name, body=search_body)
|
||||
return Response(response["hits"]["hits"], status=status.HTTP_200_OK)
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Parameters that define how the demo site will be built."""
|
||||
|
||||
NB_OBJECTS = {
|
||||
"documents": 1000,
|
||||
"services": 5,
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
# ruff: noqa: S311, S106
|
||||
"""create_demo management command"""
|
||||
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
from faker import Faker
|
||||
from opensearchpy.helpers import bulk
|
||||
|
||||
from core import factories, opensearch
|
||||
|
||||
from demo import defaults
|
||||
|
||||
fake = Faker()
|
||||
|
||||
logger = logging.getLogger("drive.commands.demo.create_demo")
|
||||
|
||||
|
||||
class BulkIndexing:
|
||||
"""A utility class to index to OpenSearch in bulk by just pushing to a queue."""
|
||||
|
||||
BATCH_SIZE = 20000
|
||||
|
||||
def __init__(self, stdout, *args, **kwargs):
|
||||
"""Define actions as a list."""
|
||||
self.actions = []
|
||||
self.stdout = stdout
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def bulk_index(self):
|
||||
"""Actually index documents in bulk to OpenSearch."""
|
||||
_success, failed = bulk(opensearch.client, self.actions, stats_only=False)
|
||||
|
||||
if failed:
|
||||
self.handle_failures(failed)
|
||||
|
||||
# Clear the actions list after bulk indexing and display progress
|
||||
self.actions.clear()
|
||||
self.stdout.write(".", ending="")
|
||||
|
||||
def handle_failures(self, failed):
|
||||
"""Handle the failed bulk operations."""
|
||||
for failure in failed:
|
||||
self.logger.error("Failed to index document: %s", failure)
|
||||
|
||||
def push(self, index_name, _id, document):
|
||||
"""Add a document to queue so that it gets created in bulk."""
|
||||
self.actions.append(
|
||||
{
|
||||
"_op_type": "index",
|
||||
"_index": index_name,
|
||||
"_id": _id,
|
||||
"_source": document,
|
||||
}
|
||||
)
|
||||
|
||||
if len(self.actions) >= self.BATCH_SIZE:
|
||||
self.bulk_index()
|
||||
|
||||
def flush(self):
|
||||
"""Flush any remaining documents in the queue."""
|
||||
if self.actions:
|
||||
self.bulk_index()
|
||||
|
||||
def __del__(self):
|
||||
"""Ensure that any remaining documents are indexed when the instance is deleted."""
|
||||
self.flush()
|
||||
|
||||
|
||||
class Timeit:
|
||||
"""A utility context manager/method decorator to time execution."""
|
||||
|
||||
total_time = 0
|
||||
|
||||
def __init__(self, stdout, sentence=None):
|
||||
"""Set the sentence to be displayed for timing information."""
|
||||
self.sentence = sentence
|
||||
self.start = None
|
||||
self.stdout = stdout
|
||||
|
||||
def __call__(self, func):
|
||||
"""Behavior on call for use as a method decorator."""
|
||||
|
||||
def timeit_wrapper(*args, **kwargs):
|
||||
"""wrapper to trigger/stop the timer before/after function call."""
|
||||
self.__enter__()
|
||||
result = func(*args, **kwargs)
|
||||
self.__exit__(None, None, None)
|
||||
return result
|
||||
|
||||
return timeit_wrapper
|
||||
|
||||
def __enter__(self):
|
||||
"""Start timer upon entering context manager."""
|
||||
self.start = time.perf_counter()
|
||||
if self.sentence:
|
||||
self.stdout.write(self.sentence, ending=".")
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_tb):
|
||||
"""Stop timer and display result upon leaving context manager."""
|
||||
if exc_type is not None:
|
||||
raise exc_type(exc_value)
|
||||
end = time.perf_counter()
|
||||
elapsed_time = end - self.start
|
||||
if self.sentence:
|
||||
self.stdout.write(f" Took {elapsed_time:g} seconds")
|
||||
|
||||
self.__class__.total_time += elapsed_time
|
||||
return elapsed_time
|
||||
|
||||
|
||||
def generate_document():
|
||||
"""Generate a realistic document dictionary faster than factory_boy does it."""
|
||||
created_at = fake.past_datetime(tzinfo=timezone.get_current_timezone())
|
||||
updated_at = fake.date_time_between(
|
||||
start_date=created_at,
|
||||
end_date=timezone.now(),
|
||||
tzinfo=timezone.get_current_timezone(),
|
||||
)
|
||||
|
||||
return {
|
||||
"title": fake.sentence(nb_words=10, variable_nb_words=True),
|
||||
"content": "\n".join(fake.paragraphs(nb=5)),
|
||||
"created_at": created_at,
|
||||
"updated_at": updated_at,
|
||||
"size": random.randint(0, 100 * 1024**2),
|
||||
"users": [str(uuid4()) for _ in range(3)],
|
||||
"groups": [slugify(fake.word()) for _ in range(3)],
|
||||
"is_public": fake.boolean(),
|
||||
}
|
||||
|
||||
|
||||
def create_demo(stdout):
|
||||
"""
|
||||
Create a database with demo data for developers to work in a realistic environment.
|
||||
"""
|
||||
opensearch.client.indices.delete("*")
|
||||
|
||||
with Timeit(stdout, "Creating services"):
|
||||
services = factories.ServiceFactory.create_batch(
|
||||
defaults.NB_OBJECTS["services"]
|
||||
)
|
||||
for service in services:
|
||||
opensearch.ensure_index_exists(service.index_name)
|
||||
opensearch.client.indices.refresh(index=service.index_name)
|
||||
|
||||
with Timeit(stdout, "Creating documents"):
|
||||
actions = BulkIndexing(stdout)
|
||||
for _ in range(defaults.NB_OBJECTS["documents"]):
|
||||
service = random.choice(services)
|
||||
document = generate_document()
|
||||
actions.push(service.index_name, uuid4(), document)
|
||||
actions.flush()
|
||||
|
||||
# Check and report on indexed documents
|
||||
total_indexed = 0
|
||||
for service in services:
|
||||
opensearch.client.indices.refresh(index=service.index_name)
|
||||
indexed = opensearch.client.count(index=service.index_name)["count"]
|
||||
stdout.write(f" - {service.index_name:s}: {indexed:d} documents")
|
||||
total_indexed += indexed
|
||||
|
||||
stdout.write(f" TOTAL: {total_indexed:d} documents")
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""A management command to create a demo database."""
|
||||
|
||||
help = __doc__
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add argument to require forcing execution when not in debug mode."""
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Force command execution despite DEBUG is set to False",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Handling of the management command."""
|
||||
if not settings.DEBUG and not options["force"]:
|
||||
raise CommandError(
|
||||
(
|
||||
"This command is not meant to be used in production environment "
|
||||
"except you know what you are doing, if so use --force parameter"
|
||||
)
|
||||
)
|
||||
|
||||
create_demo(self.stdout)
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Test the `create_demo` management command"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import override_settings
|
||||
|
||||
import pytest
|
||||
|
||||
from core import models, opensearch
|
||||
|
||||
from demo import defaults
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
TEST_NB_OBJECTS = {
|
||||
"documents": 4,
|
||||
"services": 2,
|
||||
}
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@mock.patch.dict(defaults.NB_OBJECTS, TEST_NB_OBJECTS)
|
||||
def test_commands_create_demo():
|
||||
"""The create_demo management command should create objects as expected."""
|
||||
call_command("create_demo")
|
||||
|
||||
assert models.Service.objects.count() == 2
|
||||
assert opensearch.client.count()["count"] == 4
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Drive celery configuration file."""
|
||||
import os
|
||||
|
||||
from celery import Celery
|
||||
from configurations.importer import install
|
||||
|
||||
# Set the default Django settings module for the 'celery' program.
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "drive.settings")
|
||||
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
|
||||
|
||||
install(check_options=True)
|
||||
|
||||
app = Celery("drive")
|
||||
|
||||
# Using a string here means the worker doesn't have to serialize
|
||||
# the configuration object to child processes.
|
||||
# - namespace='CELERY' means all celery-related configuration keys
|
||||
# should have a `CELERY_` prefix.
|
||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
|
||||
# Load task modules from all registered Django apps.
|
||||
app.autodiscover_tasks()
|
||||
Executable
+547
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
Django settings for drive project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.1.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import sentry_sdk
|
||||
from configurations import Configuration, values
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DATA_DIR = os.path.join("/", "data")
|
||||
|
||||
|
||||
def get_release():
|
||||
"""
|
||||
Get the current release of the application
|
||||
|
||||
By release, we mean the release from the version.json file à la Mozilla [1]
|
||||
(if any). If this file has not been found, it defaults to "NA".
|
||||
|
||||
[1]
|
||||
https://github.com/mozilla-services/Dockerflow/blob/master/docs/version_object.md
|
||||
"""
|
||||
# Try to get the current release from the version.json file generated by the
|
||||
# CI during the Docker image build
|
||||
try:
|
||||
with open(os.path.join(BASE_DIR, "version.json"), encoding="utf8") as version:
|
||||
return json.load(version)["version"]
|
||||
except FileNotFoundError:
|
||||
return "NA" # Default: not available
|
||||
|
||||
|
||||
class Base(Configuration):
|
||||
"""
|
||||
This is the base configuration every configuration (aka environment) should inherit from. It
|
||||
is recommended to configure third-party applications by creating a configuration mixins in
|
||||
./configurations and compose the Base configuration with those mixins.
|
||||
|
||||
It depends on an environment variable that SHOULD be defined:
|
||||
|
||||
* DJANGO_SECRET_KEY
|
||||
|
||||
You may also want to override default configuration by setting the following environment
|
||||
variables:
|
||||
|
||||
* DJANGO_SENTRY_DSN
|
||||
* DB_NAME
|
||||
* DB_HOST
|
||||
* DB_PASSWORD
|
||||
* DB_USER
|
||||
"""
|
||||
|
||||
DEBUG = False
|
||||
USE_SWAGGER = False
|
||||
|
||||
API_VERSION = "v1.0"
|
||||
|
||||
# Security
|
||||
ALLOWED_HOSTS = values.ListValue([])
|
||||
SECRET_KEY = values.Value(None)
|
||||
|
||||
# Application definition
|
||||
ROOT_URLCONF = "drive.urls"
|
||||
WSGI_APPLICATION = "drive.wsgi.application"
|
||||
|
||||
# Database
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": values.Value(
|
||||
"django.db.backends.postgresql_psycopg2",
|
||||
environ_name="DB_ENGINE",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"NAME": values.Value("drive", environ_name="DB_NAME", environ_prefix=None),
|
||||
"USER": values.Value("dinum", environ_name="DB_USER", environ_prefix=None),
|
||||
"PASSWORD": values.Value(
|
||||
"pass", environ_name="DB_PASSWORD", environ_prefix=None
|
||||
),
|
||||
"HOST": values.Value(
|
||||
"localhost", environ_name="DB_HOST", environ_prefix=None
|
||||
),
|
||||
"PORT": values.Value(5432, environ_name="DB_PORT", environ_prefix=None),
|
||||
}
|
||||
}
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.path.join(DATA_DIR, "static")
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = os.path.join(DATA_DIR, "media")
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "storages.backends.s3.S3Storage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": values.Value(
|
||||
"whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
environ_name="STORAGES_STATICFILES_BACKEND",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.1/topics/i18n/
|
||||
|
||||
# Languages
|
||||
LANGUAGE_CODE = values.Value("en-us")
|
||||
|
||||
# Careful! Languages should be ordered by priority, as this tuple is used to get
|
||||
# fallback/default languages throughout the app.
|
||||
LANGUAGES = values.SingleNestedTupleValue(
|
||||
(
|
||||
("en-us", _("English")),
|
||||
("fr-fr", _("French")),
|
||||
)
|
||||
)
|
||||
|
||||
LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Templates
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [os.path.join(BASE_DIR, "templates")],
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"django.template.context_processors.csrf",
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.i18n",
|
||||
"django.template.context_processors.media",
|
||||
"django.template.context_processors.request",
|
||||
"django.template.context_processors.tz",
|
||||
],
|
||||
"loaders": [
|
||||
"django.template.loaders.filesystem.Loader",
|
||||
"django.template.loaders.app_directories.Loader",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.locale.LocaleMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"dockerflow.django.middleware.DockerflowMiddleware",
|
||||
]
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
]
|
||||
|
||||
# Django applications from the highest priority to the lowest
|
||||
INSTALLED_APPS = [
|
||||
# drive
|
||||
"core",
|
||||
"demo",
|
||||
# Third party apps
|
||||
"corsheaders",
|
||||
"dockerflow.django",
|
||||
"drf_spectacular",
|
||||
"rest_framework",
|
||||
# Django
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.postgres",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.sites",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
# OIDC third party
|
||||
"mozilla_django_oidc",
|
||||
]
|
||||
|
||||
# Cache
|
||||
CACHES = {
|
||||
"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"},
|
||||
}
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"rest_framework.authentication.TokenAuthentication",
|
||||
),
|
||||
"DEFAULT_PARSER_CLASSES": [
|
||||
"rest_framework.parsers.JSONParser",
|
||||
],
|
||||
"DEFAULT_PERMISSION_CLASSES": [
|
||||
"rest_framework.permissions.IsAuthenticated",
|
||||
],
|
||||
"EXCEPTION_HANDLER": "core.api.exception_handler",
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||
"PAGE_SIZE": 20,
|
||||
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
}
|
||||
|
||||
# OpenSearch settings
|
||||
OPENSEARCH_HOST = values.Value(
|
||||
default="opensearch", environ_name="OPENSEARCH_HOST", environ_prefix=None
|
||||
)
|
||||
OPENSEARCH_PORT = values.Value(
|
||||
default=9200, environ_name="OPENSEARCH_PORT", environ_prefix=None
|
||||
)
|
||||
OPENSEARCH_USER = values.Value(
|
||||
default="admin", environ_name="OPENSEARCH_USER", environ_prefix=None
|
||||
)
|
||||
OPENSEARCH_PASSWORD = values.SecretValue(
|
||||
environ_name="OPENSEARCH_PASSWORD", environ_prefix=None
|
||||
)
|
||||
OPENSEARCH_USE_SSL = values.BooleanValue(
|
||||
default=True, environ_name="OPENSEARCH_USE_SSL", environ_prefix=None
|
||||
)
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "Drive API",
|
||||
"DESCRIPTION": "This is the drive API schema.",
|
||||
"VERSION": "0.1.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"ENABLE_DJANGO_DEPLOY_CHECK": values.BooleanValue(
|
||||
default=False,
|
||||
environ_name="SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK",
|
||||
),
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
# OTHER SETTINGS
|
||||
"SWAGGER_UI_DIST": "SIDECAR", # shorthand to use the sidecar instead
|
||||
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
|
||||
"REDOC_DIST": "SIDECAR",
|
||||
}
|
||||
|
||||
AUTH_USER_MODEL = "core.User"
|
||||
|
||||
# CORS
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(True)
|
||||
CORS_ALLOWED_ORIGINS = values.ListValue([])
|
||||
CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([])
|
||||
|
||||
# Sentry
|
||||
SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN")
|
||||
|
||||
# Celery
|
||||
CELERY_BROKER_URL = values.Value("redis://redis:6379/0")
|
||||
CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({})
|
||||
|
||||
# Session
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||
SESSION_CACHE_ALIAS = "default"
|
||||
SESSION_COOKIE_AGE = 60 * 60 * 12
|
||||
|
||||
# OIDC - Authorization Code Flow
|
||||
OIDC_CREATE_USER = values.BooleanValue(
|
||||
default=True,
|
||||
environ_name="OIDC_CREATE_USER",
|
||||
)
|
||||
OIDC_RP_SIGN_ALGO = values.Value(
|
||||
"RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None
|
||||
)
|
||||
OIDC_RP_CLIENT_ID = values.Value(
|
||||
"drive", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None
|
||||
)
|
||||
OIDC_RP_CLIENT_SECRET = values.Value(
|
||||
None,
|
||||
environ_name="OIDC_RP_CLIENT_SECRET",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_OP_JWKS_ENDPOINT = values.Value(
|
||||
environ_name="OIDC_OP_JWKS_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT = values.Value(
|
||||
environ_name="OIDC_OP_AUTHORIZATION_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_TOKEN_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_TOKEN_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_USER_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_LOGOUT_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_LOGOUT_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS = values.DictValue(
|
||||
{}, environ_name="OIDC_AUTH_REQUEST_EXTRA_PARAMS", environ_prefix=None
|
||||
)
|
||||
OIDC_RP_SCOPES = values.Value(
|
||||
"openid email", environ_name="OIDC_RP_SCOPES", environ_prefix=None
|
||||
)
|
||||
LOGIN_REDIRECT_URL = values.Value(
|
||||
None, environ_name="LOGIN_REDIRECT_URL", environ_prefix=None
|
||||
)
|
||||
LOGIN_REDIRECT_URL_FAILURE = values.Value(
|
||||
None, environ_name="LOGIN_REDIRECT_URL_FAILURE", environ_prefix=None
|
||||
)
|
||||
LOGOUT_REDIRECT_URL = values.Value(
|
||||
None, environ_name="LOGOUT_REDIRECT_URL", environ_prefix=None
|
||||
)
|
||||
OIDC_USE_NONCE = values.BooleanValue(
|
||||
default=True, environ_name="OIDC_USE_NONCE", environ_prefix=None
|
||||
)
|
||||
OIDC_REDIRECT_REQUIRE_HTTPS = values.BooleanValue(
|
||||
default=False, environ_name="OIDC_REDIRECT_REQUIRE_HTTPS", environ_prefix=None
|
||||
)
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS = values.ListValue(
|
||||
default=[], environ_name="OIDC_REDIRECT_ALLOWED_HOSTS", environ_prefix=None
|
||||
)
|
||||
OIDC_STORE_ID_TOKEN = values.BooleanValue(
|
||||
default=True, environ_name="OIDC_STORE_ID_TOKEN", environ_prefix=None
|
||||
)
|
||||
ALLOW_LOGOUT_GET_METHOD = values.BooleanValue(
|
||||
default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def ENVIRONMENT(self):
|
||||
"""Environment in which the application is launched."""
|
||||
return self.__class__.__name__.lower()
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def RELEASE(self):
|
||||
"""
|
||||
Return the release information.
|
||||
|
||||
Delegate to the module function to enable easier testing.
|
||||
"""
|
||||
return get_release()
|
||||
|
||||
@classmethod
|
||||
def post_setup(cls):
|
||||
"""Post setup configuration.
|
||||
This is the place where you can configure settings that require other
|
||||
settings to be loaded.
|
||||
"""
|
||||
super().post_setup()
|
||||
|
||||
# The SENTRY_DSN setting should be available to activate sentry for an environment
|
||||
if cls.SENTRY_DSN is not None:
|
||||
sentry_sdk.init(
|
||||
dsn=cls.SENTRY_DSN,
|
||||
environment=cls.__name__.lower(),
|
||||
release=get_release(),
|
||||
integrations=[DjangoIntegration()],
|
||||
)
|
||||
with sentry_sdk.configure_scope() as scope:
|
||||
scope.set_extra("application", "backend")
|
||||
|
||||
|
||||
class Build(Base):
|
||||
"""Settings used when the application is built.
|
||||
|
||||
This environment should not be used to run the application. Just to build it with non-blocking
|
||||
settings.
|
||||
"""
|
||||
|
||||
SECRET_KEY = values.Value("DummyKey")
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": values.Value(
|
||||
"whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
environ_name="STORAGES_STATICFILES_BACKEND",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Development(Base):
|
||||
"""
|
||||
Development environment settings
|
||||
|
||||
We set DEBUG to True and configure the server to respond from all hosts.
|
||||
"""
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
CSRF_TRUSTED_ORIGINS = ["http://localhost:8072", "http://localhost:3000"]
|
||||
DEBUG = True
|
||||
|
||||
SESSION_COOKIE_NAME = "drive_sessionid"
|
||||
|
||||
USE_SWAGGER = True
|
||||
|
||||
def __init__(self):
|
||||
# pylint: disable=invalid-name
|
||||
self.INSTALLED_APPS += ["django_extensions", "drf_spectacular_sidecar"]
|
||||
|
||||
|
||||
class Test(Base):
|
||||
"""Test environment settings"""
|
||||
|
||||
LOGGING = values.DictValue(
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"drive": {
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
PASSWORD_HASHERS = [
|
||||
"django.contrib.auth.hashers.MD5PasswordHasher",
|
||||
]
|
||||
USE_SWAGGER = True
|
||||
|
||||
CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(True)
|
||||
|
||||
def __init__(self):
|
||||
# pylint: disable=invalid-name
|
||||
self.INSTALLED_APPS += ["drf_spectacular_sidecar"]
|
||||
|
||||
|
||||
class ContinuousIntegration(Test):
|
||||
"""
|
||||
Continuous Integration environment settings
|
||||
|
||||
nota bene: it should inherit from the Test environment.
|
||||
"""
|
||||
|
||||
|
||||
class Production(Base):
|
||||
"""
|
||||
Production environment settings
|
||||
|
||||
You must define the ALLOWED_HOSTS environment variable in Production
|
||||
configuration (and derived configurations):
|
||||
ALLOWED_HOSTS=["foo.com", "foo.fr"]
|
||||
"""
|
||||
|
||||
# Security
|
||||
ALLOWED_HOSTS = values.ListValue(None)
|
||||
CSRF_TRUSTED_ORIGINS = values.ListValue([])
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
|
||||
# SECURE_PROXY_SSL_HEADER allows to fix the scheme in Django's HttpRequest
|
||||
# object when your application is behind a reverse proxy.
|
||||
#
|
||||
# Keep this SECURE_PROXY_SSL_HEADER configuration only if :
|
||||
# - your Django app is behind a proxy.
|
||||
# - your proxy strips the X-Forwarded-Proto header from all incoming requests
|
||||
# - Your proxy sets the X-Forwarded-Proto header and sends it to Django
|
||||
#
|
||||
# In other cases, you should comment the following line to avoid security issues.
|
||||
# SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# Modern browsers require to have the `secure` attribute on cookies with `Samesite=none`
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
# Privacy
|
||||
SECURE_REFERRER_POLICY = "same-origin"
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": values.Value(
|
||||
"redis://redis:6379/1",
|
||||
environ_name="REDIS_URL",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Feature(Production):
|
||||
"""
|
||||
Feature environment settings
|
||||
|
||||
nota bene: it should inherit from the Production environment.
|
||||
"""
|
||||
|
||||
|
||||
class Staging(Production):
|
||||
"""
|
||||
Staging environment settings
|
||||
|
||||
nota bene: it should inherit from the Production environment.
|
||||
"""
|
||||
|
||||
|
||||
class PreProduction(Production):
|
||||
"""
|
||||
Pre-production environment settings
|
||||
|
||||
nota bene: it should inherit from the Production environment.
|
||||
"""
|
||||
|
||||
|
||||
class Demo(Production):
|
||||
"""
|
||||
Demonstration environment settings
|
||||
|
||||
nota bene: it should inherit from the Production environment.
|
||||
"""
|
||||
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
"""URL configuration for the drive project"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from drf_spectacular.views import (
|
||||
SpectacularJSONAPIView,
|
||||
SpectacularRedocView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path(f"api/{settings.API_VERSION}/", include("core.urls")),
|
||||
]
|
||||
|
||||
if settings.USE_SWAGGER or settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path(
|
||||
f"{settings.API_VERSION}/swagger.json",
|
||||
SpectacularJSONAPIView.as_view(
|
||||
api_version=settings.API_VERSION,
|
||||
urlconf="core.urls",
|
||||
),
|
||||
name="client-api-schema",
|
||||
),
|
||||
path(
|
||||
f"{settings.API_VERSION}//swagger/",
|
||||
SpectacularSwaggerView.as_view(url_name="client-api-schema"),
|
||||
name="swagger-ui-schema",
|
||||
),
|
||||
re_path(
|
||||
f"{settings.API_VERSION}//redoc/",
|
||||
SpectacularRedocView.as_view(url_name="client-api-schema"),
|
||||
name="redoc-schema",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
WSGI config for the drive project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from configurations.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "drive.settings")
|
||||
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
drive's sandbox management script.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "drive.settings")
|
||||
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
|
||||
|
||||
from configurations.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
@@ -0,0 +1,132 @@
|
||||
#
|
||||
# drive package
|
||||
#
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "drive"
|
||||
version = "1.1.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Framework :: Django",
|
||||
"Framework :: Django :: 5",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
]
|
||||
description = "An application to print markdown to pdf from a set of managed templates."
|
||||
keywords = ["Django", "Contacts", "Templates", "RBAC"]
|
||||
license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"celery[redis]==5.3.6",
|
||||
"django-configurations==2.5",
|
||||
"django-cors-headers==4.3.1",
|
||||
"redis==5.0.3",
|
||||
"django-redis==5.4.0",
|
||||
"django==5.0.7",
|
||||
"djangorestframework==3.14.0",
|
||||
"drf_spectacular==0.26.5",
|
||||
"dockerflow==2022.8.0",
|
||||
"factory_boy==3.3.0",
|
||||
"gunicorn==22.0.0",
|
||||
"psycopg[binary]==3.1.14",
|
||||
"requests==2.32.2",
|
||||
"sentry-sdk==2.8.0",
|
||||
"url-normalize==1.4.3",
|
||||
"whitenoise==6.6.0",
|
||||
"mozilla-django-oidc==4.0.0",
|
||||
"opensearch-py==2.7.1",
|
||||
"pydantic==2.8.2",
|
||||
"pyjwt==2.9.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Bug Tracker" = "https://github.com/numerique-gouv/drive/issues/new"
|
||||
"Changelog" = "https://github.com/numerique-gouv/drive/blob/main/CHANGELOG.md"
|
||||
"Homepage" = "https://github.com/numerique-gouv/drive"
|
||||
"Repository" = "https://github.com/numerique-gouv/drive"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"django-extensions==3.2.3",
|
||||
"drf-spectacular-sidecar==2023.12.1",
|
||||
"faker==28.0.0",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==8.18.1",
|
||||
"pyfakefs==5.3.2",
|
||||
"pylint-django==2.5.5",
|
||||
"pylint==3.0.3",
|
||||
"pytest-cov==4.1.0",
|
||||
"pytest-django==4.7.0",
|
||||
"pytest==7.4.3",
|
||||
"pytest-icdiff==0.8",
|
||||
"pytest-xdist==3.5.0",
|
||||
"responses==0.24.1",
|
||||
"ruff==0.1.6",
|
||||
"types-requests==2.31.0.10",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
packages = { find = { where = ["."], exclude = ["tests"] } }
|
||||
zip-safe = true
|
||||
|
||||
[tool.distutils.bdist_wheel]
|
||||
universal = true
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
".git",
|
||||
".venv",
|
||||
"build",
|
||||
"venv",
|
||||
"__pycache__",
|
||||
"*/migrations/*",
|
||||
]
|
||||
ignore= ["DJ001", "PLR2004"]
|
||||
line-length = 88
|
||||
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"B", # flake8-bugbear
|
||||
"BLE", # flake8-blind-except
|
||||
"C4", # flake8-comprehensions
|
||||
"DJ", # flake8-django
|
||||
"I", # isort
|
||||
"PLC", # pylint-convention
|
||||
"PLE", # pylint-error
|
||||
"PLR", # pylint-refactoring
|
||||
"PLW", # pylint-warning
|
||||
"RUF100", # Ruff unused-noqa
|
||||
"RUF200", # Ruff check pyproject.toml
|
||||
"S", # flake8-bandit
|
||||
"SLF", # flake8-self
|
||||
"T20", # flake8-print
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
section-order = ["future","standard-library","django","third-party","drive","first-party","local-folder"]
|
||||
sections = { drive=["core"], django=["django"] }
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"**/tests/*" = ["S", "SLF"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = [
|
||||
"-v",
|
||||
"--cov-report",
|
||||
"term-missing",
|
||||
# Allow test files to have the same name in different directories.
|
||||
"--import-mode=importlib",
|
||||
]
|
||||
python_files = [
|
||||
"test_*.py",
|
||||
"tests.py",
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
"""Setup file for the drive module. All configuration stands in the setup.cfg file."""
|
||||
# coding: utf-8
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: drive
|
||||
version: 0.0.1
|
||||
@@ -0,0 +1,128 @@
|
||||
# Drive helm chart
|
||||
|
||||
## Parameters
|
||||
|
||||
### General configuration
|
||||
|
||||
| Name | Description | Value |
|
||||
| ------------------------------------------ | ---------------------------------------------------- | ------------------------ |
|
||||
| `image.repository` | Repository to use to pull drive's container image | `lasuite/drive-backend` |
|
||||
| `image.tag` | drive's container tag | `latest` |
|
||||
| `image.pullPolicy` | Container image pull policy | `IfNotPresent` |
|
||||
| `image.credentials.username` | Username for container registry authentication | |
|
||||
| `image.credentials.password` | Password for container registry authentication | |
|
||||
| `image.credentials.registry` | Registry url for which the credentials are specified | |
|
||||
| `image.credentials.name` | Name of the generated secret for imagePullSecrets | |
|
||||
| `nameOverride` | Override the chart name | `""` |
|
||||
| `fullnameOverride` | Override the full application name | `""` |
|
||||
| `ingress.enabled` | whether to enable the Ingress or not | `false` |
|
||||
| `ingress.className` | IngressClass to use for the Ingress | `nil` |
|
||||
| `ingress.host` | Host for the Ingress | `drive.example.com` |
|
||||
| `ingress.path` | Path to use for the Ingress | `/` |
|
||||
| `ingress.hosts` | Additional host to configure for the Ingress | `[]` |
|
||||
| `ingress.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
|
||||
| `ingress.tls.additional[].secretName` | Secret name for additional TLS config | |
|
||||
| `ingress.tls.additional[].hosts[]` | Hosts for additional TLS config | |
|
||||
| `ingress.customBackends` | Add custom backends to ingress | `[]` |
|
||||
| `ingressAdmin.enabled` | whether to enable the Ingress or not | `false` |
|
||||
| `ingressAdmin.className` | IngressClass to use for the Ingress | `nil` |
|
||||
| `ingressAdmin.host` | Host for the Ingress | `drive.example.com` |
|
||||
| `ingressAdmin.path` | Path to use for the Ingress | `/admin` |
|
||||
| `ingressAdmin.hosts` | Additional host to configure for the Ingress | `[]` |
|
||||
| `ingressAdmin.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
|
||||
| `ingressAdmin.tls.additional[].secretName` | Secret name for additional TLS config | |
|
||||
| `ingressAdmin.tls.additional[].hosts[]` | Hosts for additional TLS config | |
|
||||
|
||||
### backend
|
||||
|
||||
| Name | Description | Value |
|
||||
| ----------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| `backend.command` | Override the backend container command | `[]` |
|
||||
| `backend.args` | Override the backend container args | `[]` |
|
||||
| `backend.replicas` | Amount of backend replicas | `3` |
|
||||
| `backend.shareProcessNamespace` | Enable share process namespace between containers | `false` |
|
||||
| `backend.sidecars` | Add sidecars containers to backend deployment | `[]` |
|
||||
| `backend.securityContext` | Configure backend Pod security context | `nil` |
|
||||
| `backend.envVars` | Configure backend container environment variables | `undefined` |
|
||||
| `backend.envVars.BY_VALUE` | Example environment variable by setting value directly | |
|
||||
| `backend.envVars.FROM_CONFIGMAP.configMapKeyRef.name` | Name of a ConfigMap when configuring env vars from a ConfigMap | |
|
||||
| `backend.envVars.FROM_CONFIGMAP.configMapKeyRef.key` | Key within a ConfigMap when configuring env vars from a ConfigMap | |
|
||||
| `backend.envVars.FROM_SECRET.secretKeyRef.name` | Name of a Secret when configuring env vars from a Secret | |
|
||||
| `backend.envVars.FROM_SECRET.secretKeyRef.key` | Key within a Secret when configuring env vars from a Secret | |
|
||||
| `backend.podAnnotations` | Annotations to add to the backend Pod | `{}` |
|
||||
| `backend.service.type` | backend Service type | `ClusterIP` |
|
||||
| `backend.service.port` | backend Service listening port | `80` |
|
||||
| `backend.service.targetPort` | backend container listening port | `8000` |
|
||||
| `backend.service.annotations` | Annotations to add to the backend Service | `{}` |
|
||||
| `backend.migrate.command` | backend migrate command | `["python","manage.py","migrate","--no-input"]` |
|
||||
| `backend.migrate.restartPolicy` | backend migrate job restart policy | `Never` |
|
||||
| `backend.probes.liveness.path` | Configure path for backend HTTP liveness probe | `/__heartbeat__` |
|
||||
| `backend.probes.liveness.targetPort` | Configure port for backend HTTP liveness probe | `undefined` |
|
||||
| `backend.probes.liveness.initialDelaySeconds` | Configure initial delay for backend liveness probe | `10` |
|
||||
| `backend.probes.liveness.initialDelaySeconds` | Configure timeout for backend liveness probe | `10` |
|
||||
| `backend.probes.startup.path` | Configure path for backend HTTP startup probe | `undefined` |
|
||||
| `backend.probes.startup.targetPort` | Configure port for backend HTTP startup probe | `undefined` |
|
||||
| `backend.probes.startup.initialDelaySeconds` | Configure initial delay for backend startup probe | `undefined` |
|
||||
| `backend.probes.startup.initialDelaySeconds` | Configure timeout for backend startup probe | `undefined` |
|
||||
| `backend.probes.readiness.path` | Configure path for backend HTTP readiness probe | `/__lbheartbeat__` |
|
||||
| `backend.probes.readiness.targetPort` | Configure port for backend HTTP readiness probe | `undefined` |
|
||||
| `backend.probes.readiness.initialDelaySeconds` | Configure initial delay for backend readiness probe | `10` |
|
||||
| `backend.probes.readiness.initialDelaySeconds` | Configure timeout for backend readiness probe | `10` |
|
||||
| `backend.resources` | Resource requirements for the backend container | `{}` |
|
||||
| `backend.nodeSelector` | Node selector for the backend Pod | `{}` |
|
||||
| `backend.tolerations` | Tolerations for the backend Pod | `[]` |
|
||||
| `backend.affinity` | Affinity for the backend Pod | `{}` |
|
||||
| `backend.persistence` | Additional volumes to create and mount on the backend. Used for debugging purposes | `{}` |
|
||||
| `backend.persistence.volume-name.size` | Size of the additional volume | |
|
||||
| `backend.persistence.volume-name.type` | Type of the additional volume, persistentVolumeClaim or emptyDir | |
|
||||
| `backend.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
|
||||
| `backend.extraVolumeMounts` | Additional volumes to mount on the backend. | `[]` |
|
||||
| `backend.extraVolumes` | Additional volumes to mount on the backend. | `[]` |
|
||||
|
||||
### frontend
|
||||
|
||||
| Name | Description | Value |
|
||||
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- | ------------------------- |
|
||||
| `frontend.image.repository` | Repository to use to pull drive's frontend container image | `lasuite/drive-frontend` |
|
||||
| `frontend.image.tag` | drive's frontend container tag | `latest` |
|
||||
| `frontend.image.pullPolicy` | frontend container image pull policy | `IfNotPresent` |
|
||||
| `frontend.command` | Override the frontend container command | `[]` |
|
||||
| `frontend.args` | Override the frontend container args | `[]` |
|
||||
| `frontend.replicas` | Amount of frontend replicas | `3` |
|
||||
| `frontend.shareProcessNamespace` | Enable share process namefrontend between containers | `false` |
|
||||
| `frontend.sidecars` | Add sidecars containers to frontend deployment | `[]` |
|
||||
| `frontend.securityContext` | Configure frontend Pod security context | `nil` |
|
||||
| `frontend.envVars` | Configure frontend container environment variables | `undefined` |
|
||||
| `frontend.envVars.BY_VALUE` | Example environment variable by setting value directly | |
|
||||
| `frontend.envVars.FROM_CONFIGMAP.configMapKeyRef.name` | Name of a ConfigMap when configuring env vars from a ConfigMap | |
|
||||
| `frontend.envVars.FROM_CONFIGMAP.configMapKeyRef.key` | Key within a ConfigMap when configuring env vars from a ConfigMap | |
|
||||
| `frontend.envVars.FROM_SECRET.secretKeyRef.name` | Name of a Secret when configuring env vars from a Secret | |
|
||||
| `frontend.envVars.FROM_SECRET.secretKeyRef.key` | Key within a Secret when configuring env vars from a Secret | |
|
||||
| `frontend.podAnnotations` | Annotations to add to the frontend Pod | `{}` |
|
||||
| `frontend.service.type` | frontend Service type | `ClusterIP` |
|
||||
| `frontend.service.port` | frontend Service listening port | `80` |
|
||||
| `frontend.service.targetPort` | frontend container listening port | `8080` |
|
||||
| `frontend.service.annotations` | Annotations to add to the frontend Service | `{}` |
|
||||
| `frontend.probes` | Configure probe for frontend | `{}` |
|
||||
| `frontend.probes.liveness.path` | Configure path for frontend HTTP liveness probe | |
|
||||
| `frontend.probes.liveness.targetPort` | Configure port for frontend HTTP liveness probe | |
|
||||
| `frontend.probes.liveness.initialDelaySeconds` | Configure initial delay for frontend liveness probe | |
|
||||
| `frontend.probes.liveness.initialDelaySeconds` | Configure timeout for frontend liveness probe | |
|
||||
| `frontend.probes.startup.path` | Configure path for frontend HTTP startup probe | |
|
||||
| `frontend.probes.startup.targetPort` | Configure port for frontend HTTP startup probe | |
|
||||
| `frontend.probes.startup.initialDelaySeconds` | Configure initial delay for frontend startup probe | |
|
||||
| `frontend.probes.startup.initialDelaySeconds` | Configure timeout for frontend startup probe | |
|
||||
| `frontend.probes.readiness.path` | Configure path for frontend HTTP readiness probe | |
|
||||
| `frontend.probes.readiness.targetPort` | Configure port for frontend HTTP readiness probe | |
|
||||
| `frontend.probes.readiness.initialDelaySeconds` | Configure initial delay for frontend readiness probe | |
|
||||
| `frontend.probes.readiness.initialDelaySeconds` | Configure timeout for frontend readiness probe | |
|
||||
| `frontend.resources` | Resource requirements for the frontend container | `{}` |
|
||||
| `frontend.nodeSelector` | Node selector for the frontend Pod | `{}` |
|
||||
| `frontend.tolerations` | Tolerations for the frontend Pod | `[]` |
|
||||
| `frontend.affinity` | Affinity for the frontend Pod | `{}` |
|
||||
| `frontend.persistence` | Additional volumes to create and mount on the frontend. Used for debugging purposes | `{}` |
|
||||
| `frontend.persistence.volume-name.size` | Size of the additional volume | |
|
||||
| `frontend.persistence.volume-name.type` | Type of the additional volume, persistentVolumeClaim or emptyDir | |
|
||||
| `frontend.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
|
||||
| `frontend.extraVolumeMounts` | Additional volumes to mount on the frontend. | `[]` |
|
||||
| `frontend.extraVolumes` | Additional volumes to mount on the frontend. | `[]` |
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker image ls | grep readme-generator-for-helm
|
||||
if [ "$?" -ne "0" ]; then
|
||||
git clone https://github.com/bitnami/readme-generator-for-helm.git /tmp/readme-generator-for-helm
|
||||
cd /tmp/readme-generator-for-helm
|
||||
docker build -t readme-generator-for-helm:latest .
|
||||
cd $(dirname -- "${BASH_SOURCE[0]}")
|
||||
fi
|
||||
docker run --rm -it -v ./values.yaml:/app/values.yaml -v ./README.md:/app/README.md readme-generator-for-helm:latest readme-generator -v values.yaml -r README.md
|
||||
@@ -0,0 +1,184 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "drive.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "drive.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "drive.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
drive.labels
|
||||
*/}}
|
||||
{{- define "drive.labels" -}}
|
||||
helm.sh/chart: {{ include "drive.chart" . }}
|
||||
{{ include "drive.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "drive.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "drive.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
transform dictionnary of environment variables
|
||||
Usage : {{ include "drive.env.transformDict" .Values.envVars }}
|
||||
|
||||
Example:
|
||||
envVars:
|
||||
# Using simple strings as env vars
|
||||
ENV_VAR_NAME: "envVar value"
|
||||
# Using a value from a configMap
|
||||
ENV_VAR_FROM_CM:
|
||||
configMapKeyRef:
|
||||
name: cm-name
|
||||
key: "key_in_cm"
|
||||
# Using a value from a secret
|
||||
ENV_VAR_FROM_SECRET:
|
||||
secretKeyRef:
|
||||
name: secret-name
|
||||
key: "key_in_secret"
|
||||
*/}}
|
||||
{{- define "drive.env.transformDict" -}}
|
||||
{{- range $key, $value := . }}
|
||||
- name: {{ $key | quote }}
|
||||
{{- if $value | kindIs "map" }}
|
||||
valueFrom: {{ $value | toYaml | nindent 4 }}
|
||||
{{- else }}
|
||||
value: {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
{{/*
|
||||
drive env vars
|
||||
*/}}
|
||||
{{- define "drive.common.env" -}}
|
||||
{{- $topLevelScope := index . 0 -}}
|
||||
{{- $workerScope := index . 1 -}}
|
||||
{{- include "drive.env.transformDict" $workerScope.envVars -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
|
||||
Requires array with top level scope and component name
|
||||
*/}}
|
||||
{{- define "drive.common.labels" -}}
|
||||
{{- $topLevelScope := index . 0 -}}
|
||||
{{- $component := index . 1 -}}
|
||||
{{- include "drive.labels" $topLevelScope }}
|
||||
app.kubernetes.io/component: {{ $component }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common selector labels
|
||||
|
||||
Requires array with top level scope and component name
|
||||
*/}}
|
||||
{{- define "drive.common.selectorLabels" -}}
|
||||
{{- $topLevelScope := index . 0 -}}
|
||||
{{- $component := index . 1 -}}
|
||||
{{- include "drive.selectorLabels" $topLevelScope }}
|
||||
app.kubernetes.io/component: {{ $component }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "drive.probes.abstract" -}}
|
||||
{{- if .exec -}}
|
||||
exec:
|
||||
{{- toYaml .exec | nindent 2 }}
|
||||
{{- else if .tcpSocket -}}
|
||||
tcpSocket:
|
||||
{{- toYaml .tcpSocket | nindent 2 }}
|
||||
{{- else -}}
|
||||
httpGet:
|
||||
path: {{ .path }}
|
||||
port: {{ .targetPort }}
|
||||
{{- end }}
|
||||
initialDelaySeconds: {{ .initialDelaySeconds | eq nil | ternary 0 .initialDelaySeconds }}
|
||||
timeoutSeconds: {{ .timeoutSeconds | eq nil | ternary 1 .timeoutSeconds }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Full name for the backend
|
||||
|
||||
Requires top level scope
|
||||
*/}}
|
||||
{{- define "drive.backend.fullname" -}}
|
||||
{{ include "drive.fullname" . }}-backend
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Full name for the frontend
|
||||
|
||||
Requires top level scope
|
||||
*/}}
|
||||
{{- define "drive.frontend.fullname" -}}
|
||||
{{ include "drive.fullname" . }}-frontend
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Full name for the webrtc
|
||||
|
||||
Requires top level scope
|
||||
*/}}
|
||||
{{- define "drive.webrtc.fullname" -}}
|
||||
{{ include "drive.fullname" . }}-webrtc
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Usage : {{ include "drive.secret.dockerconfigjson.name" (dict "fullname" (include "drive.fullname" .) "imageCredentials" .Values.path.to.the.image1) }}
|
||||
*/}}
|
||||
{{- define "drive.secret.dockerconfigjson.name" }}
|
||||
{{- if (default (dict) .imageCredentials).name }}{{ .imageCredentials.name }}{{ else }}{{ .fullname | trunc 63 | trimSuffix "-" }}-dockerconfig{{ end -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Usage : {{ include "drive.secret.dockerconfigjson" (dict "fullname" (include "drive.fullname" .) "imageCredentials" .Values.path.to.the.image1) }}
|
||||
*/}}
|
||||
{{- define "drive.secret.dockerconfigjson" }}
|
||||
{{- if .imageCredentials -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ template "drive.secret.dockerconfigjson.name" (dict "fullname" .fullname "imageCredentials" .imageCredentials) }}
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install,pre-upgrade
|
||||
"helm.sh/hook-weight": "-5"
|
||||
"helm.sh/hook-delete-policy": before-hook-creation
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
data:
|
||||
.dockerconfigjson: {{ template "drive.secret.dockerconfigjson.data" .imageCredentials }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,136 @@
|
||||
{{- $envVars := include "drive.common.env" (list . .Values.backend) -}}
|
||||
{{- $fullName := include "drive.backend.fullname" . -}}
|
||||
{{- $component := "backend" -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "drive.common.labels" (list . $component) | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.backend.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "drive.common.selectorLabels" (list . $component) | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- with .Values.backend.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "drive.common.selectorLabels" (list . $component) | nindent 8 }}
|
||||
spec:
|
||||
{{- if $.Values.image.credentials }}
|
||||
imagePullSecrets:
|
||||
- name: {{ include "drive.secret.dockerconfigjson.name" (dict "fullname" (include "drive.fullname" .) "imageCredentials" $.Values.image.credentials) }}
|
||||
{{- end}}
|
||||
shareProcessNamespace: {{ .Values.backend.shareProcessNamespace }}
|
||||
containers:
|
||||
{{- with .Values.backend.sidecars }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ (.Values.backend.image | default dict).repository | default .Values.image.repository }}:{{ (.Values.backend.image | default dict).tag | default .Values.image.tag }}"
|
||||
imagePullPolicy: {{ (.Values.backend.image | default dict).pullPolicy | default .Values.image.pullPolicy }}
|
||||
{{- with .Values.backend.command }}
|
||||
command:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.args }}
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars}}
|
||||
{{- $envVars | indent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.backend.service.targetPort }}
|
||||
protocol: TCP
|
||||
{{- if .Values.backend.probes.liveness }}
|
||||
livenessProbe:
|
||||
{{- include "drive.probes.abstract" (merge .Values.backend.probes.liveness (dict "targetPort" .Values.backend.service.targetPort )) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.backend.probes.readiness }}
|
||||
readinessProbe:
|
||||
{{- include "drive.probes.abstract" (merge .Values.backend.probes.readiness (dict "targetPort" .Values.backend.service.targetPort )) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.backend.probes.startup }}
|
||||
startupProbe:
|
||||
{{- include "drive.probes.abstract" (merge .Values.backend.probes.startup (dict "targetPort" .Values.backend.service.targetPort )) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.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.backend.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
mountPath: "{{ $volume.mountPath }}"
|
||||
{{- end }}
|
||||
{{- range .Values.backend.extraVolumeMounts }}
|
||||
- name: {{ .name }}
|
||||
mountPath: {{ .mountPath }}
|
||||
subPath: {{ .subPath | default "" }}
|
||||
readOnly: {{ .readOnly }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
{{- range $index, $value := .Values.mountFiles }}
|
||||
- name: "files-{{ $index }}"
|
||||
configMap:
|
||||
name: "{{ include "drive.fullname" $ }}-files-{{ $index }}"
|
||||
{{- end }}
|
||||
{{- range $name, $volume := .Values.backend.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
{{- if eq $volume.type "emptyDir" }}
|
||||
emptyDir: {}
|
||||
{{- else }}
|
||||
persistentVolumeClaim:
|
||||
claimName: "{{ $fullName }}-{{ $name }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.backend.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 }}
|
||||
@@ -0,0 +1,121 @@
|
||||
{{- $envVars := include "drive.common.env" (list . .Values.backend) -}}
|
||||
{{- $fullName := include "drive.backend.fullname" . -}}
|
||||
{{- $component := "backend" -}}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ $fullName }}-migrate
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
{{- with .Values.backend.migrateJobAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "drive.common.labels" (list . $component) | nindent 4 }}
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- with .Values.backend.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "drive.common.selectorLabels" (list . $component) | nindent 8 }}
|
||||
spec:
|
||||
{{- if $.Values.image.credentials }}
|
||||
imagePullSecrets:
|
||||
- name: {{ include "drive.secret.dockerconfigjson.name" (dict "fullname" (include "drive.fullname" .) "imageCredentials" $.Values.image.credentials) }}
|
||||
{{- end}}
|
||||
shareProcessNamespace: {{ .Values.backend.shareProcessNamespace }}
|
||||
containers:
|
||||
{{- with .Values.backend.sidecars }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ (.Values.backend.image | default dict).repository | default .Values.image.repository }}:{{ (.Values.backend.image | default dict).tag | default .Values.image.tag }}"
|
||||
imagePullPolicy: {{ (.Values.backend.image | default dict).pullPolicy | default .Values.image.pullPolicy }}
|
||||
{{- with .Values.backend.migrate.command }}
|
||||
command:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.args }}
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars}}
|
||||
{{- $envVars | indent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.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.backend.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
mountPath: "{{ $volume.mountPath }}"
|
||||
{{- end }}
|
||||
{{- range .Values.backend.extraVolumeMounts }}
|
||||
- name: {{ .name }}
|
||||
mountPath: {{ .mountPath }}
|
||||
subPath: {{ .subPath | default "" }}
|
||||
readOnly: {{ .readOnly }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
restartPolicy: {{ .Values.backend.migrate.restartPolicy }}
|
||||
volumes:
|
||||
{{- range $index, $value := .Values.mountFiles }}
|
||||
- name: "files-{{ $index }}"
|
||||
configMap:
|
||||
name: "{{ include "drive.fullname" $ }}-files-{{ $index }}"
|
||||
{{- end }}
|
||||
{{- range $name, $volume := .Values.backend.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
{{- if eq $volume.type "emptyDir" }}
|
||||
emptyDir: {}
|
||||
{{- else }}
|
||||
persistentVolumeClaim:
|
||||
claimName: "{{ $fullName }}-{{ $name }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.backend.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 }}
|
||||
@@ -0,0 +1,121 @@
|
||||
{{- $envVars := include "drive.common.env" (list . .Values.backend) -}}
|
||||
{{- $fullName := include "drive.backend.fullname" . -}}
|
||||
{{- $component := "backend" -}}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ $fullName }}-createsuperuser
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
{{- with .Values.backend.migrateJobAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "drive.common.labels" (list . $component) | nindent 4 }}
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- with .Values.backend.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "drive.common.selectorLabels" (list . $component) | nindent 8 }}
|
||||
spec:
|
||||
{{- if $.Values.image.credentials }}
|
||||
imagePullSecrets:
|
||||
- name: {{ include "drive.secret.dockerconfigjson.name" (dict "fullname" (include "drive.fullname" .) "imageCredentials" $.Values.image.credentials) }}
|
||||
{{- end}}
|
||||
shareProcessNamespace: {{ .Values.backend.shareProcessNamespace }}
|
||||
containers:
|
||||
{{- with .Values.backend.sidecars }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ (.Values.backend.image | default dict).repository | default .Values.image.repository }}:{{ (.Values.backend.image | default dict).tag | default .Values.image.tag }}"
|
||||
imagePullPolicy: {{ (.Values.backend.image | default dict).pullPolicy | default .Values.image.pullPolicy }}
|
||||
{{- with .Values.backend.createsuperuser.command }}
|
||||
command:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.args }}
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars}}
|
||||
{{- $envVars | indent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.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.backend.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
mountPath: "{{ $volume.mountPath }}"
|
||||
{{- end }}
|
||||
{{- range .Values.backend.extraVolumeMounts }}
|
||||
- name: {{ .name }}
|
||||
mountPath: {{ .mountPath }}
|
||||
subPath: {{ .subPath | default "" }}
|
||||
readOnly: {{ .readOnly }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
restartPolicy: {{ .Values.backend.createsuperuser.restartPolicy }}
|
||||
volumes:
|
||||
{{- range $index, $value := .Values.mountFiles }}
|
||||
- name: "files-{{ $index }}"
|
||||
configMap:
|
||||
name: "{{ include "drive.fullname" $ }}-files-{{ $index }}"
|
||||
{{- end }}
|
||||
{{- range $name, $volume := .Values.backend.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
{{- if eq $volume.type "emptyDir" }}
|
||||
emptyDir: {}
|
||||
{{- else }}
|
||||
persistentVolumeClaim:
|
||||
claimName: "{{ $fullName }}-{{ $name }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.backend.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 }}
|
||||
@@ -0,0 +1,21 @@
|
||||
{{- $envVars := include "drive.common.env" (list . .Values.backend) -}}
|
||||
{{- $fullName := include "drive.backend.fullname" . -}}
|
||||
{{- $component := "backend" -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "drive.common.labels" (list . $component) | nindent 4 }}
|
||||
annotations:
|
||||
{{- toYaml $.Values.backend.service.annotations | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.backend.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.backend.service.port }}
|
||||
targetPort: {{ .Values.backend.service.targetPort }}
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "drive.common.selectorLabels" (list . $component) | nindent 4 }}
|
||||
@@ -0,0 +1,136 @@
|
||||
{{- $envVars := include "drive.common.env" (list . .Values.frontend) -}}
|
||||
{{- $fullName := include "drive.frontend.fullname" . -}}
|
||||
{{- $component := "frontend" -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "drive.common.labels" (list . $component) | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.frontend.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "drive.common.selectorLabels" (list . $component) | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- with .Values.frontend.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "drive.common.selectorLabels" (list . $component) | nindent 8 }}
|
||||
spec:
|
||||
{{- if $.Values.image.credentials }}
|
||||
imagePullSecrets:
|
||||
- name: {{ include "drive.secret.dockerconfigjson.name" (dict "fullname" (include "drive.fullname" .) "imageCredentials" $.Values.image.credentials) }}
|
||||
{{- end}}
|
||||
shareProcessNamespace: {{ .Values.frontend.shareProcessNamespace }}
|
||||
containers:
|
||||
{{- with .Values.frontend.sidecars }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ (.Values.frontend.image | default dict).repository | default .Values.image.repository }}:{{ (.Values.frontend.image | default dict).tag | default .Values.image.tag }}"
|
||||
imagePullPolicy: {{ (.Values.frontend.image | default dict).pullPolicy | default .Values.image.pullPolicy }}
|
||||
{{- with .Values.frontend.command }}
|
||||
command:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.frontend.args }}
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars}}
|
||||
{{- $envVars | indent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.frontend.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.frontend.service.targetPort }}
|
||||
protocol: TCP
|
||||
{{- if .Values.frontend.probes.liveness }}
|
||||
livenessProbe:
|
||||
{{- include "drive.probes.abstract" (merge .Values.frontend.probes.liveness (dict "targetPort" .Values.frontend.service.targetPort )) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.frontend.probes.readiness }}
|
||||
readinessProbe:
|
||||
{{- include "drive.probes.abstract" (merge .Values.frontend.probes.readiness (dict "targetPort" .Values.frontend.service.targetPort )) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.frontend.probes.startup }}
|
||||
startupProbe:
|
||||
{{- include "drive.probes.abstract" (merge .Values.frontend.probes.startup (dict "targetPort" .Values.frontend.service.targetPort )) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.frontend.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.frontend.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
mountPath: "{{ $volume.mountPath }}"
|
||||
{{- end }}
|
||||
{{- range .Values.frontend.extraVolumeMounts }}
|
||||
- name: {{ .name }}
|
||||
mountPath: {{ .mountPath }}
|
||||
subPath: {{ .subPath | default "" }}
|
||||
readOnly: {{ .readOnly }}
|
||||
{{- end }}
|
||||
{{- with .Values.frontend.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.frontend.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.frontend.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
{{- range $index, $value := .Values.mountFiles }}
|
||||
- name: "files-{{ $index }}"
|
||||
configMap:
|
||||
name: "{{ include "drive.fullname" $ }}-files-{{ $index }}"
|
||||
{{- end }}
|
||||
{{- range $name, $volume := .Values.frontend.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
{{- if eq $volume.type "emptyDir" }}
|
||||
emptyDir: {}
|
||||
{{- else }}
|
||||
persistentVolumeClaim:
|
||||
claimName: "{{ $fullName }}-{{ $name }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.frontend.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 }}
|
||||
@@ -0,0 +1,21 @@
|
||||
{{- $envVars := include "drive.common.env" (list . .Values.frontend) -}}
|
||||
{{- $fullName := include "drive.frontend.fullname" . -}}
|
||||
{{- $component := "frontend" -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "drive.common.labels" (list . $component) | nindent 4 }}
|
||||
annotations:
|
||||
{{- toYaml $.Values.frontend.service.annotations | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.frontend.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.frontend.service.port }}
|
||||
targetPort: {{ .Values.frontend.service.targetPort }}
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "drive.common.selectorLabels" (list . $component) | nindent 4 }}
|
||||
@@ -0,0 +1,118 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "drive.fullname" . -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "drive.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
tls:
|
||||
{{- if .Values.ingress.host }}
|
||||
- secretName: {{ $fullName }}-tls
|
||||
hosts:
|
||||
- {{ .Values.ingress.host | quote }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingress.tls.additional }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- if .Values.ingress.host }}
|
||||
- host: {{ .Values.ingress.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingress.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "drive.frontend.fullname" . }}
|
||||
port:
|
||||
number: {{ .Values.frontend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "drive.frontend.fullname" . }}
|
||||
servicePort: {{ .Values.frontend.service.port }}
|
||||
{{- end }}
|
||||
- path: /api
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "drive.backend.fullname" . }}
|
||||
port:
|
||||
number: {{ .Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "drive.backend.fullname" . }}
|
||||
servicePort: {{ .Values.backend.service.port }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.customBackends }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $.Values.ingress.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "drive.frontend.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.frontend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "drive.frontend.fullname" $ }}
|
||||
servicePort: {{ $.Values.frontend.service.port }}
|
||||
{{- end }}
|
||||
- path: /api
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "drive.backend.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "drive.backend.fullname" $ }}
|
||||
servicePort: {{ $.Values.backend.service.port }}
|
||||
{{- end }}
|
||||
{{- with $.Values.ingress.customBackends }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
{{- if .Values.ingressAdmin.enabled -}}
|
||||
{{- $fullName := include "drive.fullname" . -}}
|
||||
{{- if and .Values.ingressAdmin.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingressAdmin.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingressAdmin.annotations "kubernetes.io/ingress.class" .Values.ingressAdmin.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}-admin
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "drive.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingressAdmin.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingressAdmin.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingressAdmin.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingressAdmin.tls.enabled }}
|
||||
tls:
|
||||
{{- if .Values.ingressAdmin.host }}
|
||||
- secretName: {{ $fullName }}-tls
|
||||
hosts:
|
||||
- {{ .Values.ingressAdmin.host | quote }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressAdmin.tls.additional }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- if .Values.ingressAdmin.host }}
|
||||
- host: {{ .Values.ingressAdmin.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingressAdmin.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "drive.backend.fullname" . }}
|
||||
port:
|
||||
number: {{ .Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "drive.backend.fullname" . }}
|
||||
servicePort: {{ .Values.backend.service.port }}
|
||||
{{- end }}
|
||||
- path: /static
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "drive.backend.fullname" . }}
|
||||
port:
|
||||
number: {{ .Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "drive.backend.fullname" . }}
|
||||
servicePort: {{ .Values.backend.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressAdmin.hosts }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $.Values.ingressAdmin.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "drive.backend.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "drive.backend.fullname" $ }}
|
||||
servicePort: {{ $.Values.backend.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
{{- if .Values.ingressMedia.enabled -}}
|
||||
{{- $fullName := include "drive.fullname" . -}}
|
||||
{{- if and .Values.ingressMedia.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingressMedia.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingressMedia.annotations "kubernetes.io/ingress.class" .Values.ingressMedia.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}-media
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "drive.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingressMedia.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingressMedia.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingressMedia.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingressMedia.tls.enabled }}
|
||||
tls:
|
||||
{{- if .Values.ingressMedia.host }}
|
||||
- secretName: {{ $fullName }}-tls
|
||||
hosts:
|
||||
- {{ .Values.ingressMedia.host | quote }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressMedia.tls.additional }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- if .Values.ingressMedia.host }}
|
||||
- host: {{ .Values.ingressMedia.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingressMedia.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "drive.backend.fullname" . }}
|
||||
port:
|
||||
number: {{ .Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "drive.backend.fullname" . }}
|
||||
servicePort: {{ .Values.backend.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressMedia.hosts }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $.Values.ingressMedia.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "drive.backend.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "drive.backend.fullname" $ }}
|
||||
servicePort: {{ $.Values.backend.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{{ if .Values.htaccess }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: htaccess
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
stringData:
|
||||
auth: {{ .Values.htaccess }}
|
||||
{{ end }}
|
||||
@@ -0,0 +1,265 @@
|
||||
# Default values for drive.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
## @section General configuration
|
||||
|
||||
## @param image.repository Repository to use to pull drive's container image
|
||||
## @param image.tag drive's container tag
|
||||
## @param image.pullPolicy Container image pull policy
|
||||
## @extra image.credentials.username Username for container registry authentication
|
||||
## @extra image.credentials.password Password for container registry authentication
|
||||
## @extra image.credentials.registry Registry url for which the credentials are specified
|
||||
## @extra image.credentials.name Name of the generated secret for imagePullSecrets
|
||||
image:
|
||||
repository: lasuite/drive-backend
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
## @param nameOverride Override the chart name
|
||||
## @param fullnameOverride Override the full application name
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
## @skip commonEnvVars
|
||||
commonEnvVars: &commonEnvVars
|
||||
<<: []
|
||||
|
||||
## @param ingress.enabled whether to enable the Ingress or not
|
||||
## @param ingress.className IngressClass to use for the Ingress
|
||||
## @param ingress.host Host for the Ingress
|
||||
## @param ingress.path Path to use for the Ingress
|
||||
ingress:
|
||||
enabled: false
|
||||
className: null
|
||||
host: drive.example.com
|
||||
path: /
|
||||
## @param ingress.hosts Additional host to configure for the Ingress
|
||||
hosts: []
|
||||
# - chart-example.local
|
||||
## @param ingress.tls.enabled Weather to enable TLS for the Ingress
|
||||
## @skip ingress.tls.additional
|
||||
## @extra ingress.tls.additional[].secretName Secret name for additional TLS config
|
||||
## @extra ingress.tls.additional[].hosts[] Hosts for additional TLS config
|
||||
tls:
|
||||
enabled: true
|
||||
additional: []
|
||||
|
||||
## @param ingress.customBackends Add custom backends to ingress
|
||||
customBackends: []
|
||||
|
||||
|
||||
## @param ingressAdmin.enabled whether to enable the Ingress or not
|
||||
## @param ingressAdmin.className IngressClass to use for the Ingress
|
||||
## @param ingressAdmin.host Host for the Ingress
|
||||
## @param ingressAdmin.path Path to use for the Ingress
|
||||
ingressAdmin:
|
||||
enabled: false
|
||||
className: null
|
||||
host: drive.example.com
|
||||
path: /admin
|
||||
## @param ingressAdmin.hosts Additional host to configure for the Ingress
|
||||
hosts: [ ]
|
||||
# - chart-example.local
|
||||
## @param ingressAdmin.tls.enabled Weather to enable TLS for the Ingress
|
||||
## @skip ingressAdmin.tls.additional
|
||||
## @extra ingressAdmin.tls.additional[].secretName Secret name for additional TLS config
|
||||
## @extra ingressAdmin.tls.additional[].hosts[] Hosts for additional TLS config
|
||||
tls:
|
||||
enabled: true
|
||||
additional: []
|
||||
|
||||
|
||||
## @section backend
|
||||
|
||||
backend:
|
||||
|
||||
## @param backend.command Override the backend container command
|
||||
command: []
|
||||
|
||||
## @param backend.args Override the backend container args
|
||||
args: []
|
||||
|
||||
## @param backend.replicas Amount of backend replicas
|
||||
replicas: 3
|
||||
|
||||
## @param backend.shareProcessNamespace Enable share process namespace between containers
|
||||
shareProcessNamespace: false
|
||||
|
||||
## @param backend.sidecars Add sidecars containers to backend deployment
|
||||
sidecars: []
|
||||
|
||||
## @param backend.migrateJobAnnotations Annotations for the migrate job
|
||||
migrateJobAnnotations: {}
|
||||
|
||||
## @param backend.securityContext Configure backend Pod security context
|
||||
securityContext: null
|
||||
|
||||
## @param backend.envVars Configure backend container environment variables
|
||||
## @extra backend.envVars.BY_VALUE Example environment variable by setting value directly
|
||||
## @extra backend.envVars.FROM_CONFIGMAP.configMapKeyRef.name Name of a ConfigMap when configuring env vars from a ConfigMap
|
||||
## @extra backend.envVars.FROM_CONFIGMAP.configMapKeyRef.key Key within a ConfigMap when configuring env vars from a ConfigMap
|
||||
## @extra backend.envVars.FROM_SECRET.secretKeyRef.name Name of a Secret when configuring env vars from a Secret
|
||||
## @extra backend.envVars.FROM_SECRET.secretKeyRef.key Key within a Secret when configuring env vars from a Secret
|
||||
## @skip backend.envVars
|
||||
envVars:
|
||||
<<: *commonEnvVars
|
||||
|
||||
## @param backend.podAnnotations Annotations to add to the backend Pod
|
||||
podAnnotations: {}
|
||||
|
||||
## @param backend.service.type backend Service type
|
||||
## @param backend.service.port backend Service listening port
|
||||
## @param backend.service.targetPort backend container listening port
|
||||
## @param backend.service.annotations Annotations to add to the backend Service
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
targetPort: 8000
|
||||
annotations: {}
|
||||
|
||||
## @param backend.migrate.command backend migrate command
|
||||
## @param backend.migrate.restartPolicy backend migrate job restart policy
|
||||
migrate:
|
||||
command:
|
||||
- "python"
|
||||
- "manage.py"
|
||||
- "migrate"
|
||||
- "--no-input"
|
||||
restartPolicy: Never
|
||||
|
||||
## @param backend.probes.liveness.path [nullable] Configure path for backend HTTP liveness probe
|
||||
## @param backend.probes.liveness.targetPort [nullable] Configure port for backend HTTP liveness probe
|
||||
## @param backend.probes.liveness.initialDelaySeconds [nullable] Configure initial delay for backend liveness probe
|
||||
## @param backend.probes.liveness.initialDelaySeconds [nullable] Configure timeout for backend liveness probe
|
||||
## @param backend.probes.startup.path [nullable] Configure path for backend HTTP startup probe
|
||||
## @param backend.probes.startup.targetPort [nullable] Configure port for backend HTTP startup probe
|
||||
## @param backend.probes.startup.initialDelaySeconds [nullable] Configure initial delay for backend startup probe
|
||||
## @param backend.probes.startup.initialDelaySeconds [nullable] Configure timeout for backend startup probe
|
||||
## @param backend.probes.readiness.path [nullable] Configure path for backend HTTP readiness probe
|
||||
## @param backend.probes.readiness.targetPort [nullable] Configure port for backend HTTP readiness probe
|
||||
## @param backend.probes.readiness.initialDelaySeconds [nullable] Configure initial delay for backend readiness probe
|
||||
## @param backend.probes.readiness.initialDelaySeconds [nullable] Configure timeout for backend readiness probe
|
||||
probes:
|
||||
liveness:
|
||||
path: /__heartbeat__
|
||||
initialDelaySeconds: 10
|
||||
readiness:
|
||||
path: /__lbheartbeat__
|
||||
initialDelaySeconds: 10
|
||||
|
||||
## @param backend.resources Resource requirements for the backend container
|
||||
resources: {}
|
||||
|
||||
## @param backend.nodeSelector Node selector for the backend Pod
|
||||
nodeSelector: {}
|
||||
|
||||
## @param backend.tolerations Tolerations for the backend Pod
|
||||
tolerations: []
|
||||
|
||||
## @param backend.affinity Affinity for the backend Pod
|
||||
affinity: {}
|
||||
|
||||
## @param backend.persistence Additional volumes to create and mount on the backend. Used for debugging purposes
|
||||
## @extra backend.persistence.volume-name.size Size of the additional volume
|
||||
## @extra backend.persistence.volume-name.type Type of the additional volume, persistentVolumeClaim or emptyDir
|
||||
## @extra backend.persistence.volume-name.mountPath Path where the volume should be mounted to
|
||||
persistence: {}
|
||||
|
||||
## @param backend.extraVolumeMounts Additional volumes to mount on the backend.
|
||||
extraVolumeMounts: []
|
||||
|
||||
## @param backend.extraVolumes Additional volumes to mount on the backend.
|
||||
extraVolumes: []
|
||||
|
||||
|
||||
## @section frontend
|
||||
|
||||
frontend:
|
||||
## @param frontend.image.repository Repository to use to pull drive's frontend container image
|
||||
## @param frontend.image.tag drive's frontend container tag
|
||||
## @param frontend.image.pullPolicy frontend container image pull policy
|
||||
image:
|
||||
repository: lasuite/drive-frontend
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
## @param frontend.command Override the frontend container command
|
||||
command: []
|
||||
|
||||
## @param frontend.args Override the frontend container args
|
||||
args: []
|
||||
|
||||
## @param frontend.replicas Amount of frontend replicas
|
||||
replicas: 3
|
||||
|
||||
## @param frontend.shareProcessNamespace Enable share process namefrontend between containers
|
||||
shareProcessNamespace: false
|
||||
|
||||
## @param frontend.sidecars Add sidecars containers to frontend deployment
|
||||
sidecars: []
|
||||
|
||||
## @param frontend.securityContext Configure frontend Pod security context
|
||||
securityContext: null
|
||||
|
||||
## @param frontend.envVars Configure frontend container environment variables
|
||||
## @extra frontend.envVars.BY_VALUE Example environment variable by setting value directly
|
||||
## @extra frontend.envVars.FROM_CONFIGMAP.configMapKeyRef.name Name of a ConfigMap when configuring env vars from a ConfigMap
|
||||
## @extra frontend.envVars.FROM_CONFIGMAP.configMapKeyRef.key Key within a ConfigMap when configuring env vars from a ConfigMap
|
||||
## @extra frontend.envVars.FROM_SECRET.secretKeyRef.name Name of a Secret when configuring env vars from a Secret
|
||||
## @extra frontend.envVars.FROM_SECRET.secretKeyRef.key Key within a Secret when configuring env vars from a Secret
|
||||
## @skip frontend.envVars
|
||||
envVars:
|
||||
<<: *commonEnvVars
|
||||
|
||||
## @param frontend.podAnnotations Annotations to add to the frontend Pod
|
||||
podAnnotations: {}
|
||||
|
||||
## @param frontend.service.type frontend Service type
|
||||
## @param frontend.service.port frontend Service listening port
|
||||
## @param frontend.service.targetPort frontend container listening port
|
||||
## @param frontend.service.annotations Annotations to add to the frontend Service
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
annotations: {}
|
||||
|
||||
## @param frontend.probes Configure probe for frontend
|
||||
## @extra frontend.probes.liveness.path Configure path for frontend HTTP liveness probe
|
||||
## @extra frontend.probes.liveness.targetPort Configure port for frontend HTTP liveness probe
|
||||
## @extra frontend.probes.liveness.initialDelaySeconds Configure initial delay for frontend liveness probe
|
||||
## @extra frontend.probes.liveness.initialDelaySeconds Configure timeout for frontend liveness probe
|
||||
## @extra frontend.probes.startup.path Configure path for frontend HTTP startup probe
|
||||
## @extra frontend.probes.startup.targetPort Configure port for frontend HTTP startup probe
|
||||
## @extra frontend.probes.startup.initialDelaySeconds Configure initial delay for frontend startup probe
|
||||
## @extra frontend.probes.startup.initialDelaySeconds Configure timeout for frontend startup probe
|
||||
## @extra frontend.probes.readiness.path Configure path for frontend HTTP readiness probe
|
||||
## @extra frontend.probes.readiness.targetPort Configure port for frontend HTTP readiness probe
|
||||
## @extra frontend.probes.readiness.initialDelaySeconds Configure initial delay for frontend readiness probe
|
||||
## @extra frontend.probes.readiness.initialDelaySeconds Configure timeout for frontend readiness probe
|
||||
probes: {}
|
||||
|
||||
## @param frontend.resources Resource requirements for the frontend container
|
||||
resources: {}
|
||||
|
||||
## @param frontend.nodeSelector Node selector for the frontend Pod
|
||||
nodeSelector: {}
|
||||
|
||||
## @param frontend.tolerations Tolerations for the frontend Pod
|
||||
tolerations: []
|
||||
|
||||
## @param frontend.affinity Affinity for the frontend Pod
|
||||
affinity: {}
|
||||
|
||||
## @param frontend.persistence Additional volumes to create and mount on the frontend. Used for debugging purposes
|
||||
## @extra frontend.persistence.volume-name.size Size of the additional volume
|
||||
## @extra frontend.persistence.volume-name.type Type of the additional volume, persistentVolumeClaim or emptyDir
|
||||
## @extra frontend.persistence.volume-name.mountPath Path where the volume should be mounted to
|
||||
persistence: {}
|
||||
|
||||
## @param frontend.extraVolumeMounts Additional volumes to mount on the frontend.
|
||||
extraVolumeMounts: []
|
||||
|
||||
## @param frontend.extraVolumes Additional volumes to mount on the frontend.
|
||||
extraVolumes: []
|
||||
@@ -0,0 +1,62 @@
|
||||
djangoSuperUserEmail: ENC[AES256_GCM,data:7b1xfYmr1g0RlBmsHBRA39ZPV/6+1DrtHQ==,iv:/GW7oLxPTZYmRWVPvyAQMoZl1owHM4Fo0XAOtyEh2rA=,tag:DaqoW+dglyAOXMm5+mrDfA==,type:str]
|
||||
djangoSuperUserPass: ENC[AES256_GCM,data:RQgX,iv:q3CdfmwGfHSTjLXTimDk/1MyoFLviRuwmZa2E7GUzhY=,tag:HCtdtqgSxdJIHFhI8xpegQ==,type:str]
|
||||
djangoSecretKey: ENC[AES256_GCM,data:mtJCf6mKfj/fJkg4wmfIvvU1vkUEF77BI8TUFikp/M3nPveDXhKmy3Cw3cXFpOYiFZ0=,iv:qwPRKsPS1Jhylj5asbmknXm1xOX3nfp9iccuorUrcj0=,tag:ENVfAt4i3PttoqD8+Kc4wQ==,type:str]
|
||||
oidc:
|
||||
clientId: ENC[AES256_GCM,data:wndPCbysbWDybdHglcG+wkMWk1rrD40hKqFxct9T3TLEGOk/,iv:RH1OdBX1GYIT90sSq0AGz49fFi6dL0m49Pegs6Ko9tQ=,tag:/tKytQwoZkBX1Tf96gAjIA==,type:str]
|
||||
clientSecret: ENC[AES256_GCM,data:MUJ0wsg+LC2QZ1jZ0Twd3FS3dQevmJq9/97qVI3ARHuJIVlQz0Qah4vE7/iR+sn7ME2o1s1AzV4c1Yx/F3nHBg==,iv:LvinICSzF/8EvrHZD4Jp6lt7g3yxSOEgVHPrc3SShjo=,tag:yvkyyBXmhEkmGL7jZevUCA==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMMjFCeWhkUmRWTnlIM1JM
|
||||
dVFock1DWmtXQnpQZWZMWW1YdndhSS93MlVFCmxKVDUwOUt0NjJIZiswSm5aRi9U
|
||||
VEllelBZVmFKdVFzcVJPUm50VHo5RTgKLS0tIDlkU3htTEdSREFOSUxlTGVtUm1n
|
||||
RzJZbzhFcDNZKzdxMWFHTWx6Uy9GVFkKTw8LbhzAACp0NUHDfNcXpZyr2pJyNxxw
|
||||
C7j/UB0cAejlSJHaUUiZ6TEcslXRpqnNagwUw4z/uzo7m4temay22A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQQjBNMnVlNURQVWdjSyty
|
||||
RGozcmN5eTUwRHJIWnhhc1E3U1NXQ3AwTWxBCnFjbmJNZnFiRVJ6VHhmQmt1Vk5n
|
||||
OTVXWVh3RzhoMWNrbUl6OHphTjFLQVUKLS0tIGJjUlNhK0dHQ2R3SCtrbTRnaFJT
|
||||
Q1pyRXhSVm8xQWk2NG1MK0srVU1pL2sKkoxGCM00UM2leTNCn5H8499uwJw1NIXs
|
||||
PoRNgplehrHFptrAwGEpSYMXbxu88N7EWa/rtOp+sHWK5zpxscMkjA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzYnpkYnJnYnJjVFRHRzRa
|
||||
N09JOXVnQkVrcVcwdk9kR1k1azNib2lkMVZFCmhvOHlpVnJ0RlRpYWZ1TkVoaklV
|
||||
NmNzY3BEeWN1MUtKWmZFT2RaMUxBRW8KLS0tIG92ZmhsZ29LSkRSREhiaG9kWXhH
|
||||
akREb0ttYVpNWTJHb1pjaWRFbWpxUjgKgZp3cN2rZw4ktbpb5cUnDEtsT/KWszGi
|
||||
pmpJHgsMADigyUc+Pjw+1pwpn0FtXVEXGedbf8bBuJavvbS2PuJBsg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxaHZJeStiVnBzTGNTNzdo
|
||||
UDFVTU51ZWp0WWorUnBlSzVBSU9IU2JnbUNNCkpMZGdNV3FUYkZOcWNLK0JWci81
|
||||
WGNwYi9Jb0QrV0lkUzNJWTcrUjIzUmMKLS0tIHlTKzNsVzNsSGFuYjJ0RFp0Y1Nr
|
||||
a1VOcDBPTTYvNjkxN092N1UrYk1CM2cKNifC3ZLOrFTFKA9iKg8nPpZb+3DxnTwq
|
||||
grsrxQa40b/Vv/aPoiPBMeSENDcH48X/EhMFNKX7dvl+7HEaY+QPlA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1hnhuzj96ktkhpyygvmz0x9h8mfvssz7ss6emmukags644mdhf4msajk93r
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoZ2ZlcllJeGlKUDNxUk1w
|
||||
ekZ3TSttaXREV1FBRWwzNW54cjlYbHpLdWpRCnhSL2hEVVBEWEJKQWF0YTk1YzhJ
|
||||
RTBGN25sT0hBM3V4QndiTVkveDBwQ2cKLS0tIEdoZGRLRXdCME1wcUJHQXhtSHBQ
|
||||
UVEyNUVIanF6Z3ZSUjU1aTk0NFRBR0EKGuH5vzOV9lP/qRew0maECapKtLILaf/4
|
||||
XoSgPnjh8pIbJG7i9VKnFORlzkNJ6OPhZlX3ax15hd1qQv0PSCMBDA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2024-07-02T10:03:17Z"
|
||||
mac: ENC[AES256_GCM,data:qx236E1cFtBmbYyUf6B95/Fwu2hoi9ZAhUcYiY/tsG9h1+kwXntfkvbH3ekyI7A5ZrpJXMeQZ7gLc+ohci4m5Ju+/G39MjMt+ww0Y6gBMqe59YlHfeFD2mYsnn9j1pqtbrIJ6+8fLDmhaXtGtXP3qRmFTc9LwL6Rm+5gn8cjcnA=,iv:TC7zBnQ0hRz0JSytrYVnyJiI1eMWRTBqctLajZYUhvU=,tag:wCBeo2xD5UpdRqGjkZxbXA==,type:str]
|
||||
pgp: []
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.8.1
|
||||
@@ -0,0 +1,89 @@
|
||||
image:
|
||||
repository: localhost:5001/drive-backend
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
|
||||
backend:
|
||||
replicas: 1
|
||||
envVars:
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: https://drive.127.0.0.1.nip.io,http://drive.127.0.0.1.nip.io
|
||||
DJANGO_CONFIGURATION: Production
|
||||
DJANGO_ALLOWED_HOSTS: "*"
|
||||
DJANGO_SECRET_KEY: {{ .Values.djangoSecretKey }}
|
||||
DJANGO_SETTINGS_MODULE: drive.settings
|
||||
DJANGO_SUPERUSER_PASSWORD: admin
|
||||
DJANGO_EMAIL_HOST: "mailcatcher"
|
||||
DJANGO_EMAIL_PORT: 1025
|
||||
DJANGO_EMAIL_USE_SSL: False
|
||||
OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize
|
||||
OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token
|
||||
OIDC_OP_USER_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/userinfo
|
||||
OIDC_OP_LOGOUT_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/session/end
|
||||
OIDC_RP_CLIENT_ID: {{ .Values.oidc.clientId }}
|
||||
OIDC_RP_CLIENT_SECRET: {{ .Values.oidc.clientSecret }}
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email"
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://drive.127.0.0.1.nip.io
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||
LOGIN_REDIRECT_URL: https://drive.127.0.0.1.nip.io
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://drive.127.0.0.1.nip.io
|
||||
LOGOUT_REDIRECT_URL: https://drive.127.0.0.1.nip.io
|
||||
DB_HOST: postgres-postgresql
|
||||
DB_NAME: drive
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
POSTGRES_DB: drive
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
|
||||
migrate:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
python manage.py migrate --no-input &&
|
||||
python manage.py create_demo --force
|
||||
restartPolicy: Never
|
||||
|
||||
command:
|
||||
- "gunicorn"
|
||||
- "-c"
|
||||
- "/usr/local/etc/gunicorn/drive.py"
|
||||
- "drive.wsgi:application"
|
||||
- "--reload"
|
||||
|
||||
createsuperuser:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
shell -c "from core.models import User; not User.objects.filter(username='admin').exists() and User.objects.create_superuser('admin', 'admin@example.com', 'admin')"
|
||||
restartPolicy: Never
|
||||
|
||||
frontend:
|
||||
envVars:
|
||||
PORT: 8080
|
||||
NEXT_PUBLIC_API_ORIGIN: https://drive.127.0.0.1.nip.io
|
||||
NEXT_PUBLIC_SIGNALING_URL: wss://drive.127.0.0.1.nip.io/ws
|
||||
|
||||
replicas: 1
|
||||
command:
|
||||
- yarn
|
||||
- dev
|
||||
|
||||
image:
|
||||
repository: localhost:5001/drive-frontend
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
host: drive.127.0.0.1.nip.io
|
||||
|
||||
ingressAdmin:
|
||||
enabled: true
|
||||
host: drive.127.0.0.1.nip.io
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../../secrets/numerique-gouv/impress/env/preprod/secrets.enc.yaml
|
||||
@@ -0,0 +1,137 @@
|
||||
image:
|
||||
repository: lasuite/drive-backend
|
||||
pullPolicy: Always
|
||||
tag: "v1.1.0"
|
||||
|
||||
backend:
|
||||
migrateJobAnnotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: HookSucceeded
|
||||
envVars:
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: http://drive-preprod.beta.numerique.gouv.fr,https://drive-preprod.beta.numerique.gouv.fr
|
||||
DJANGO_CONFIGURATION: Production
|
||||
DJANGO_ALLOWED_HOSTS: "*"
|
||||
DJANGO_SUPERUSER_EMAIL:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: DJANGO_SUPERUSER_EMAIL
|
||||
DJANGO_SECRET_KEY:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: DJANGO_SECRET_KEY
|
||||
DJANGO_SETTINGS_MODULE: drive.settings
|
||||
DJANGO_SUPERUSER_PASSWORD:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: DJANGO_SUPERUSER_PASSWORD
|
||||
DJANGO_EMAIL_HOST: "snap-mail.numerique.gouv.fr"
|
||||
DJANGO_EMAIL_PORT: 465
|
||||
DJANGO_EMAIL_USE_SSL: True
|
||||
DJANGO_SILENCED_SYSTEM_CHECKS: security.W008,security.W004
|
||||
OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize
|
||||
OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token
|
||||
OIDC_OP_USER_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/userinfo
|
||||
OIDC_OP_LOGOUT_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/session/end
|
||||
OIDC_RP_CLIENT_ID:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: OIDC_RP_CLIENT_ID
|
||||
OIDC_RP_CLIENT_SECRET:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: OIDC_RP_CLIENT_SECRET
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email"
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://drive-preprod.beta.numerique.gouv.fr
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||
LOGIN_REDIRECT_URL: https://drive-preprod.beta.numerique.gouv.fr
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://drive-preprod.beta.numerique.gouv.fr
|
||||
LOGOUT_REDIRECT_URL: https://drive-preprod.beta.numerique.gouv.fr
|
||||
DB_HOST:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: host
|
||||
DB_NAME:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: database
|
||||
DB_USER:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: username
|
||||
DB_PASSWORD:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: password
|
||||
DB_PORT:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: port
|
||||
POSTGRES_USER:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: username
|
||||
POSTGRES_DB:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: database
|
||||
POSTGRES_PASSWORD:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: password
|
||||
REDIS_URL:
|
||||
secretKeyRef:
|
||||
name: redis.redis.libre.sh
|
||||
key: url
|
||||
AWS_S3_ENDPOINT_URL:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: url
|
||||
AWS_S3_ACCESS_KEY_ID:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: accessKey
|
||||
AWS_S3_SECRET_ACCESS_KEY:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: secretKey
|
||||
AWS_STORAGE_BUCKET_NAME:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: bucket
|
||||
AWS_S3_REGION_NAME: local
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
|
||||
createsuperuser:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
|
||||
restartPolicy: Never
|
||||
|
||||
frontend:
|
||||
image:
|
||||
repository: lasuite/drive-frontend
|
||||
pullPolicy: Always
|
||||
tag: "v1.1.0"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
host: drive-preprod.beta.numerique.gouv.fr
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/auth-type: basic
|
||||
nginx.ingress.kubernetes.io/auth-secret: htaccess
|
||||
nginx.ingress.kubernetes.io/auth-realm: 'Authentication Required'
|
||||
|
||||
ingressAdmin:
|
||||
enabled: true
|
||||
host: drive-preprod.beta.numerique.gouv.fr
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/start
|
||||
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/auth
|
||||
@@ -0,0 +1 @@
|
||||
../../../../secrets/numerique-gouv/impress/env/production/secrets.enc.yaml
|
||||
@@ -0,0 +1,137 @@
|
||||
image:
|
||||
repository: lasuite/drive-backend
|
||||
pullPolicy: Always
|
||||
tag: "v1.1.0"
|
||||
|
||||
backend:
|
||||
migrateJobAnnotations:
|
||||
argocd.argoproj.io/hook: PostSync
|
||||
argocd.argoproj.io/hook-delete-policy: HookSucceeded
|
||||
envVars:
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: https://docs.numerique.gouv.fr
|
||||
DJANGO_CONFIGURATION: Production
|
||||
DJANGO_ALLOWED_HOSTS: "*"
|
||||
DJANGO_SECRET_KEY:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: DJANGO_SECRET_KEY
|
||||
DJANGO_SETTINGS_MODULE: drive.settings
|
||||
DJANGO_SUPERUSER_EMAIL:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: DJANGO_SUPERUSER_EMAIL
|
||||
DJANGO_SUPERUSER_PASSWORD:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: DJANGO_SUPERUSER_PASSWORD
|
||||
DJANGO_EMAIL_HOST: "snap-mail.numerique.gouv.fr"
|
||||
DJANGO_EMAIL_PORT: 465
|
||||
DJANGO_EMAIL_USE_SSL: True
|
||||
DJANGO_SILENCED_SYSTEM_CHECKS: security.W008,security.W004
|
||||
OIDC_OP_JWKS_ENDPOINT: https://auth.agentconnect.gouv.fr/api/v2/jwks
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://auth.agentconnect.gouv.fr/api/v2/authorize
|
||||
OIDC_OP_TOKEN_ENDPOINT: https://auth.agentconnect.gouv.fr/api/v2/token
|
||||
OIDC_OP_USER_ENDPOINT: https://auth.agentconnect.gouv.fr/api/v2/userinfo
|
||||
OIDC_OP_LOGOUT_ENDPOINT: https://auth.agentconnect.gouv.fr/api/v2/session/end
|
||||
OIDC_RP_CLIENT_ID:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: OIDC_RP_CLIENT_ID
|
||||
OIDC_RP_CLIENT_SECRET:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: OIDC_RP_CLIENT_SECRET
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email"
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://docs.numerique.gouv.fr
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||
LOGIN_REDIRECT_URL: https://docs.numerique.gouv.fr
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://docs.numerique.gouv.fr
|
||||
LOGOUT_REDIRECT_URL: https://docs.numerique.gouv.fr
|
||||
DB_HOST:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: host
|
||||
DB_NAME:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: database
|
||||
DB_USER:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: username
|
||||
DB_PASSWORD:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: password
|
||||
DB_PORT:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: port
|
||||
POSTGRES_USER:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: username
|
||||
POSTGRES_DB:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: database
|
||||
POSTGRES_PASSWORD:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: password
|
||||
REDIS_URL:
|
||||
secretKeyRef:
|
||||
name: redis.redis.libre.sh
|
||||
key: url
|
||||
AWS_S3_ENDPOINT_URL:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: url
|
||||
AWS_S3_ACCESS_KEY_ID:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: accessKey
|
||||
AWS_S3_SECRET_ACCESS_KEY:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: secretKey
|
||||
AWS_STORAGE_BUCKET_NAME:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: bucket
|
||||
AWS_S3_REGION_NAME: local
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
|
||||
createsuperuser:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
|
||||
restartPolicy: Never
|
||||
|
||||
frontend:
|
||||
image:
|
||||
repository: lasuite/drive-frontend
|
||||
pullPolicy: Always
|
||||
tag: "v1.1.0"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
host: docs.numerique.gouv.fr
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt
|
||||
nginx.ingress.kubernetes.io/auth-type: basic
|
||||
nginx.ingress.kubernetes.io/auth-secret: htaccess
|
||||
nginx.ingress.kubernetes.io/auth-realm: 'Authentication Required'
|
||||
|
||||
ingressAdmin:
|
||||
enabled: true
|
||||
host: docs.numerique.gouv.fr
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt
|
||||
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy.beta.numerique.gouv.fr/oauth2/start
|
||||
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy.beta.numerique.gouv.fr/oauth2/auth
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../../secrets/numerique-gouv/impress/env/staging/secrets.enc.yaml
|
||||
@@ -0,0 +1,134 @@
|
||||
image:
|
||||
repository: lasuite/drive-backend
|
||||
pullPolicy: Always
|
||||
tag: "main"
|
||||
|
||||
backend:
|
||||
migrateJobAnnotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: HookSucceeded
|
||||
envVars:
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: http://drive-staging.beta.numerique.gouv.fr,https://drive-staging.beta.numerique.gouv.fr
|
||||
DJANGO_CONFIGURATION: Production
|
||||
DJANGO_ALLOWED_HOSTS: "*"
|
||||
DJANGO_SECRET_KEY:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: DJANGO_SECRET_KEY
|
||||
DJANGO_SETTINGS_MODULE: drive.settings
|
||||
DJANGO_SUPERUSER_EMAIL:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: DJANGO_SUPERUSER_EMAIL
|
||||
DJANGO_SUPERUSER_PASSWORD:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: DJANGO_SUPERUSER_PASSWORD
|
||||
DJANGO_EMAIL_HOST: "snap-mail.numerique.gouv.fr"
|
||||
DJANGO_EMAIL_PORT: 465
|
||||
DJANGO_EMAIL_USE_SSL: True
|
||||
DJANGO_SILENCED_SYSTEM_CHECKS: security.W008,security.W004
|
||||
OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize
|
||||
OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token
|
||||
OIDC_OP_USER_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/userinfo
|
||||
OIDC_OP_LOGOUT_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/session/end
|
||||
OIDC_RP_CLIENT_ID:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: OIDC_RP_CLIENT_ID
|
||||
OIDC_RP_CLIENT_SECRET:
|
||||
secretKeyRef:
|
||||
name: backend
|
||||
key: OIDC_RP_CLIENT_SECRET
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email"
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://drive-staging.beta.numerique.gouv.fr
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||
LOGIN_REDIRECT_URL: https://drive-staging.beta.numerique.gouv.fr
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://drive-staging.beta.numerique.gouv.fr
|
||||
LOGOUT_REDIRECT_URL: https://drive-staging.beta.numerique.gouv.fr
|
||||
DB_HOST:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: host
|
||||
DB_NAME:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: database
|
||||
DB_USER:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: username
|
||||
DB_PASSWORD:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: password
|
||||
DB_PORT:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: port
|
||||
POSTGRES_USER:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: username
|
||||
POSTGRES_DB:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: database
|
||||
POSTGRES_PASSWORD:
|
||||
secretKeyRef:
|
||||
name: postgresql.postgres.libre.sh
|
||||
key: password
|
||||
REDIS_URL:
|
||||
secretKeyRef:
|
||||
name: redis.redis.libre.sh
|
||||
key: url
|
||||
AWS_S3_ENDPOINT_URL:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: url
|
||||
AWS_S3_ACCESS_KEY_ID:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: accessKey
|
||||
AWS_S3_SECRET_ACCESS_KEY:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: secretKey
|
||||
AWS_STORAGE_BUCKET_NAME:
|
||||
secretKeyRef:
|
||||
name: drive-media-storage.bucket.libre.sh
|
||||
key: bucket
|
||||
AWS_S3_REGION_NAME: local
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
|
||||
createsuperuser:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
|
||||
restartPolicy: Never
|
||||
|
||||
frontend:
|
||||
image:
|
||||
repository: lasuite/drive-frontend
|
||||
pullPolicy: Always
|
||||
tag: "main"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
host: drive-staging.beta.numerique.gouv.fr
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
|
||||
ingressAdmin:
|
||||
enabled: true
|
||||
host: drive-staging.beta.numerique.gouv.fr
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/start
|
||||
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/auth
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: v2
|
||||
name: extra
|
||||
description: A Helm chart to add some manifests to drive
|
||||
type: application
|
||||
version: 0.1.0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user