diff --git a/.github/workflows/publish-dockerimage.yml b/.github/workflows/publish-dockerimage.yml new file mode 100644 index 00000000..015578bd --- /dev/null +++ b/.github/workflows/publish-dockerimage.yml @@ -0,0 +1,135 @@ +name: Create and publish a Docker image + +# Configures this workflow to run every time new release is published +on: + push: + tags: + - 'v*.*.*' + - 'v*.*.*-*' + +# Defines two custom environment variables for the workflow. +# These are used for the Container registry domain, +# and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + ODOOD_DLANG_COMPILER: ldc-1.39.0 + +# There is a single job in this workflow. +# It's configured to run on the latest available version of Ubuntu. +jobs: + build-ubuntu-20_04: + name: Build Ubuntu:20.04 + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + + - name: Install D compiler + uses: dlang-community/setup-dlang@v1 + with: + compiler: ${{ env.ODOOD_DLANG_COMPILER }} + + - name: Install system dependencies + uses: lyricwulf/abc@v1 + with: + linux: libzip-dev libpq-dev python3-dev + + - name: Build Odood + run: | + dub build -b release --d-version OdoodInDocker + + - name: Upload Odood compiled assets + uses: actions/upload-artifact@v3 + with: + name: odood-amd64 + path: | + build/odood + + build-and-push-images: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + attestations: write + id-token: write + needs: + - build-ubuntu-20_04 + strategy: + matrix: + # TODO: Build base image (without Odoo, only with odood itself) and then use it as base for odoo images + odoo_version: + - "15.0" + - "16.0" + - "17.0" + - "18.0" + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: 'Download artifacts for amd64' + uses: actions/download-artifact@v3 + with: + name: odood-amd64 + path: docker/bin/ + + - name: Make odood executable + run: | + chmod a+x docker/bin/odood + + # Uses the `docker/login-action` action to log in to the Container registry + # using the account and password that will publish the packages. + # Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) + # to extract tags and labels that will be applied to the specified image. + # The `id` "meta" allows the output of this step to be referenced in a subsequent step. + # The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/odoo/${{ matrix.odoo_version }} + + - name: Prepare debian dependencies dependencies + id: prepare_deb_deps + run: | + echo "universal_deb_deps=$(cat .ci/deps/universal-deb.txt | tr '\n' ' ')" >> $GITHUB_OUTPUT + + # This step uses the `docker/build-push-action` action to build the image, + # based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. + # For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" + # in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + id: push + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: ./docker + push: true + build-args: | + ODOO_VERSION=${{ matrix.odoo_version }} + ODOOD_DEPENDENCIES=${{ steps.prepare_deb_deps.outputs.universal_deb_deps }} + tags: ${{ steps.meta.outputs.tags }} + labels: | + ${{ steps.meta.outputs.labels }} + odoo_version=${{ matrix.odoo_version }} + + # This step generates an artifact attestation for the image, + # which is an unforgeable statement about where and how it was built. + # It increases supply chain security for people who consume the image. + # For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}/odoo/${{ matrix.odoo_version }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ef6cbb0..3ab1d0cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ on: push: tags: - 'v*.*.*' - - 'v*.*.*-RC*' + - 'v*.*.*-*' env: ODOOD_DLANG_COMPILER: ldc-1.39.0 @@ -90,10 +90,10 @@ jobs: - name: 'Show directory structure' run: ls -R - - name: Check RC Release + - name: Check RC/alpha Release id: check-rc-release run: | - if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+-(rc|RC)[0-9]+$ ]]; then + if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+-(rc|RC|[0-9a-zA-Z\.\-]+)[0-9]+$ ]]; then echo "is_rc_release=true" >> $GITHUB_OUTPUT elif [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "is_rc_release=false" >> $GITHUB_OUTPUT diff --git a/.github/workflows/test-deployments.yml b/.github/workflows/test-deployments.yml new file mode 100644 index 00000000..2d0e4f08 --- /dev/null +++ b/.github/workflows/test-deployments.yml @@ -0,0 +1,94 @@ + +# Workflow to test Odoo deployments on various OS +name: Tests OS deployments +on: + push: + branches: + - '*' +env: + ODOOD_DLANG_COMPILER: ldc-1.39.0 + +# In this test, we build Odood on Ubuntu 20.04 and then try to run +# produced binary on different distros and versions +jobs: + # Compile test builds on ubuntu 20.04 + compile-ubuntu-20_04: + name: Build Ubuntu:20.04 + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + + - name: Install D compiler + uses: dlang-community/setup-dlang@v1 + with: + compiler: ${{ env.ODOOD_DLANG_COMPILER }} + + - name: Install system dependencies + uses: lyricwulf/abc@v1 + with: + linux: libzip-dev libpq-dev python3-dev + + - name: Build Odood + run: | + dub build -b unittest-cov + + - name: Upload Odood compiled assets + uses: actions/upload-artifact@v3 + with: + name: odood-ubuntu-20.04 + path: | + build/odood + + # Run integration tests for different operation systems + run-deployment-tests: + name: Run deployment tests + strategy: + fail-fast: false + matrix: + image: + - "debian:bullseye" + - "ubuntu:20.04" + - "ubuntu:22.04" + - "ubuntu:24.04" + odoo_version: + - "18.0" + - "17.0" + - "16.0" + runs-on: ubuntu-22.04 + needs: + - compile-ubuntu-20_04 + container: + image: ${{ matrix.image }} + env: + DEBIAN_FRONTEND: 'noninteractive' + steps: + - uses: actions/checkout@v3 + + - name: 'Download artifacts for ubuntu 20.04' + uses: actions/download-artifact@v3 + with: + name: odood-ubuntu-20.04 + path: build + + - name: Update apt registry + run: apt-get update + + - name: Install system dependencies + run: apt-get install --no-install-recommends -yq sudo logrotate + + - name: Install package dependencies + run: apt-get install --no-install-recommends -yq $(cat .ci/deps/universal-deb.txt) + + - name: Make test build executable + run: chmod a+x ./build/odood + + - name: Deploy with init-script + run: ./build/odood deploy -v ${{ matrix.odoo_version }} --supervisor=init-script + + # TODO: Add test some operations with this instance + + - name: Upload coverage to codecov + uses: codecov/codecov-action@v3 + with: + flags: deployment-tests-${{ matrix.image }}-${{ matrix.odoo_version }} + name: odood-deployment-tests-${{ matrix.image }}-${{ matrix.odoo_version }} diff --git a/.github/workflows/test-os-integrations.yml b/.github/workflows/test-os-integrations.yml index 6d826621..f35a5782 100644 --- a/.github/workflows/test-os-integrations.yml +++ b/.github/workflows/test-os-integrations.yml @@ -48,7 +48,6 @@ jobs: - "debian:bullseye" - "ubuntu:20.04" - "ubuntu:22.04" - - "ubuntu:23.04" - "ubuntu:24.04" runs-on: ubuntu-22.04 needs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 083818fd..ddb9ab7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## Release 0.2.0 (2024-12-12) + +### Added + +- New experimental command `odood deploy` that could be used to deploy production-ready Odoo instance. +- Added experimental support for Odoo 18 +- Added new command `odood repo fix-series` that allows to set series for all modules in repo to project's serie. +- Added automatic builds of docker images with pre-installed Odoo. + +### Changed + +- Pre-commit related commands moved to `pre-commit` subcommand. + Thus, following commands now available to work with pre-commit: + - `odood pre-commit init` + - `odood pre-commit set-up` + - `odood pre-commit run` +- Change command `odood server run`. Command uses `execv` to run Odoo, + thus, Odoo process will replace Odood process. Thus, option `--detach` + is not available here. If you want to start Odoo in background, then + `odood server start` command exists. Instead, this command (`odood server run`) + is designed to run Odoo with provided args in same way as you run Odoo binary directly. + For example, following command + `odood server run -- -d my_database --install=crm --stop-after-init`, + that will install `crm` module, will be translated to `odoo -d my_database --install=crm --stop-after-init`, + that will be ran inside virtualenv of current Odood project. + - Added new option `--ignore-running` that allows to ignore server running. + - Removed option `--detach` as it does not have sense. Use `odood server start` instead. +- Changed generation of default test db name. + Before it was: `odood-odood-test` + Now it will be: `-odood-test` + +--- + ## Release 0.1.0 (2024-08-15) ### Added @@ -13,7 +46,6 @@ - New command `odood venv run` that allows to run any command from current venv. - New command `odood repo run-pre-commit` to run [pre-commit](https://pre-commit.com/) for the repo. - ### Changed - Database restoration reimplemented in D, diff --git a/README.md b/README.md index 786f15e2..59b5b8ac 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,30 @@ Following features currently implemented: - [x] Linters - use pre-commit and per-repo configurations, instead of directly running linters +## Supported Odoo versions + +- Odoo 7.0 (partial) +- Odoo 8.0 (best efforts) +- Odoo 9.0 (best efforts) +- Odoo 10.0 (best efforts) +- Odoo 11.0 (best efforts) +- Odoo 12.0 (tested) +- Odoo 13.0 (tested) +- Odoo 14.0 (tested) +- Odoo 15.0 (tested) +- Odoo 16.0 (tested) +- Odoo 17.0 (tested) +- Odoo 18.0 (experimental) + +## Prebuild docker-images with preinstalled Odoo and Odood + +You can use on of [prebuilt images](https://github.com/katyukha?tab=packages&repo_name=Odood) to run Odoo managed by Odood in containers: +- [Odoo 18](https://ghcr.io/katyukha/odood/odoo/18.0) +- [Odoo 17](https://ghcr.io/katyukha/odood/odoo/17.0) +- [Odoo 16](https://ghcr.io/katyukha/odood/odoo/16.0) +- [Odoo 15](https://ghcr.io/katyukha/odood/odoo/15.0) + + ## Installation (as Debian Package) This is the recommended way to install Odood. @@ -69,7 +93,7 @@ project with Odood is to run command `odood discover odoo-helper` somewhere insi ## Quick start -Use following command to create new local odoo instance: +Use following command to create new local (development) odoo instance: ```bash odood init -v 17 -i odoo-17.0 --db-user=odoo17 --db-password=odoo --http-port=17069 --create-db-user @@ -78,6 +102,11 @@ odood init -v 17 -i odoo-17.0 --db-user=odoo17 --db-password=odoo --http-port=17 This command will create new virtual environment for Odoo and install odoo there. Also, this command will automatically create database user for this Odoo instance. +For production installations, you can use command `odood deploy` that will +deploy Odoo of specified version to machine where this command is running. +For example: `odood deploy -v 17 --supervisor=systemd --local-postgres --enable-logrotate` +But this command is still experimental. + Next, change current working directory to directory where we installed Odoo: ```bash @@ -118,6 +147,69 @@ See help for this command for more info: odood addons --help ``` +It is possible to easily add repositories with third-party addons to odood projects. +To do this, following command could be used + +```bash +odood repo add --help +``` + +For example, if you want to add [crnd-inc/generic-addons](https://github.com/crnd-inc/generic-addons) +you can run following command: + +```bash +odood repo add --github crnd-inc/generic-addons +``` + +## Example setup for docker compose + +Odood has prebuilt docker images, that could be used to easily run Odoo powered by Odoo inside docker-based infrastructure. + +See examples directory for more details. + +Example `docker-compose.yml`: + +```yml +version: '3' + +volumes: + odood-example-db-data: + odood-example-odoo-data: + +services: + odood-example-db: + image: postgres:15 + container_name: odood-example-db + environment: + - POSTGRES_USER=odoo + - POSTGRES_PASSWORD=odoo-db-pass + + # this is needed to avoid auto-creation of database by postgres itself + # databases must be created by Odoo only + - POSTGRES_DB=postgres + volumes: + - odood-example-db-data:/var/lib/postgresql/data + restart: "no" + + odood-example-odoo: + image: ghcr.io/katyukha/odood/odoo/17.0:latest + container_name: odood-example-odoo + depends_on: + - odood-example-db + environment: + ODOOD_OPT_DB_HOST: odood-example-db + ODOOD_OPT_DB_USER: odoo + ODOOD_OPT_DB_PASSWORD: odoo-db-pass + ODOOD_OPT_ADMIN_PASSWD: admin + ODOOD_OPT_WORKERS: "1" + ports: + - "8069:8069" + volumes: + - odood-example-odoo-data:/opt/odoo/data + restart: "no" +``` + + ## Level up your service quality Level up your service quality with [Service Desk](https://crnd.pro/solutions/service-desk) / [ITSM](https://crnd.pro/itsm) solution by [CR&D](https://crnd.pro/). diff --git a/build_docker.bash b/build_docker.bash new file mode 100755 index 00000000..7398b967 --- /dev/null +++ b/build_docker.bash @@ -0,0 +1,6 @@ +#!/bin/bash + + +dub build --d-version OdoodInDocker; +cp ./build/odood ./docker/bin/odood; +(cd ./docker && docker build -t tmp-odood:17 --build-arg ODOO_VERSION=17 --build-arg "ODOOD_DEPENDENCIES=$(cat ../.ci/deps/universal-deb.txt)" .) diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 00000000..5e56e040 --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1 @@ +/bin diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..ffc7f3d2 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,44 @@ +FROM ubuntu:24.04 + +ARG ODOO_VERSION +ARG ODOOD_DEPENDENCIES + +RUN apt-get update -qq && \ + apt-get install -qqq -y --no-install-recommends --auto-remove \ + locales \ + wget \ + ca-certificates \ + gnupg \ + lsb-release \ + xfonts-75dpi xfonts-base libx11-6 libxcb1 libxext6 libxrender1 \ + tzdata && \ + locale-gen en_US.UTF-8 && \ + locale-gen en_GB.UTF-8 && \ + update-locale LANG="en_US.UTF-8" && update-locale LANGUAGE="en_US:en" \ + echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | \ + apt-key add - && \ + apt-get update -qq && \ + apt-get install -qqq -y --no-install-recommends --auto-remove $ODOOD_DEPENDENCIES && \ + wget --quiet -O /tmp/wkhtmltopdf.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_$(dpkg --print-architecture).deb && \ + dpkg -i /tmp/wkhtmltopdf.deb && \ + rm -f /tmp/wkhtmltopdf.deb && \ + rm -rf /var/lib/apt/lists/* && \ + rm -rf /root/.cache/pip/* + +# Set corect locale-related environment variables +ENV LANG="en_US.UTF-8" LANGUAGE="en_US:en" LC_ALL="en_US.UTF-8" + +COPY ./bin/odood /usr/bin/odood + +RUN /usr/bin/odood deploy -v "$ODOO_VERSION" --supervisor=odood --log-to-stderr + +#WORKDIR /opt/odoo + +EXPOSE 8069 +EXPOSE 8071 +EXPOSE 8072 + +VOLUME ["/opt/odoo/data", "/opt/odoo/backups"] + +CMD ["/usr/bin/odood", "server", "run", "--config-from-env"] diff --git a/docker/bin/.gitkeep b/docker/bin/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/dub.sdl b/dub.sdl index ea6097a9..69625fc8 100644 --- a/dub.sdl +++ b/dub.sdl @@ -7,11 +7,13 @@ license "MPL-2.0" dependency ":exception" version="*" dependency ":tipy" version="*" dependency ":utils" version="*" +dependency ":git" version="*" dependency ":lib" version="*" dependency ":cli" version="*" subPackage "./subpackages/exception" subPackage "./subpackages/tipy" subPackage "./subpackages/utils" +subPackage "./subpackages/git" subPackage "./subpackages/lib" subPackage "./subpackages/cli" diff --git a/dub.selections.json b/dub.selections.json index f337765d..f4944869 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -5,7 +5,7 @@ "bindbc-loader": "1.1.5", "cachetools": "0.3.1", "colored": "0.0.31", - "commandr": {"version":"~master","repository":"git+https://github.com/katyukha/commandr.git"}, + "commandr": "1.1.0", "dini": "2.0.0", "dpq": "0.11.6", "dshould": "1.7.2", @@ -15,8 +15,8 @@ "requests": "2.1.3", "silly": "1.1.1", "tabletool": "0.5.0", - "thepath": "1.2.0", - "theprocess": "0.0.5", + "thepath": "2.0.0", + "theprocess": "0.0.7", "tinyendian": "0.2.0", "unit-threaded": "2.1.9", "zipper": "0.0.5" diff --git a/examples/docker-compose/odoo-and-db-ssl/docker-compose.yml b/examples/docker-compose/odoo-and-db-ssl/docker-compose.yml new file mode 100644 index 00000000..9cfaf453 --- /dev/null +++ b/examples/docker-compose/odoo-and-db-ssl/docker-compose.yml @@ -0,0 +1,73 @@ +version: '3' + +volumes: + odood-example-ssl-db-data: + odood-example-ssl-odoo-data: + +services: + odood-example-ssl-db: + image: postgres:15 + container_name: odood-example-ssl-db + environment: + - POSTGRES_USER=odoo + - POSTGRES_PASSWORD=odoo-db-pass + + # this is needed to avoid auto-creation of database by postgres itself + # databases must be created by Odoo only + - POSTGRES_DB=postgres + volumes: + - odood-example-ssl-db-data:/var/lib/postgresql/data + restart: "no" + + odood-example-ssl-odoo: + image: ghcr.io/katyukha/odood/odoo/17.0:latest + container_name: odood-example-ssl-odoo + labels: + - "traefik.enable=true" + - "traefik.http.routers.odoo-route.rule=Host(`localhost`)" + - "traefik.http.routers.odoo-route.service=odoo-service" + - "traefik.http.routers.odoo-route.entrypoints=webssl" + - "traefik.http.routers.odoo-route.tls=true" + - "traefik.http.services.odoo-service.loadbalancer.server.port=8069" + - "traefik.http.routers.odoo-ge-route.rule=Host(`localhost`) && Path(`/websocket`)" + - "traefik.http.routers.odoo-ge-route.service=odoo-ge-service" + - "traefik.http.routers.odoo-ge-route.entrypoints=webssl" + - "traefik.http.routers.odoo-ge-route.tls=true" + - "traefik.http.services.odoo-ge-service.loadbalancer.server.port=8072" + + depends_on: + - odood-example-ssl-db + environment: + ODOOD_OPT_DB_HOST: odood-example-ssl-db + ODOOD_OPT_DB_USER: odoo + ODOOD_OPT_DB_PASSWORD: odoo-db-pass + ODOOD_OPT_ADMIN_PASSWD: admin + ODOOD_OPT_WORKERS: "1" + ODOOD_OPT_PROXY_MODE: True + volumes: + - odood-example-ssl-odoo-data:/opt/odoo/data + restart: "no" + + odood-example-ssl-traefik: + image: "traefik:v3.2" + container_name: "odood-example-ssl-traefik" + command: + #- "--log.level=DEBUG" + #- "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.file.filename=/traefik-certs.yml" + - "--entryPoints.web.address=:80" + - "--entryPoints.webssl.address=:443" + - "--entrypoints.web.http.redirections.entrypoint.to=webssl" + - "--entrypoints.web.http.redirections.entrypoint.scheme=https" + ports: + - "80:80" + - "443:443" + - "8080:8080" + volumes: + - "./traefik/traefik-certs.yml:/traefik-certs.yml" + - "./traefik/certs/:/certs/" + - "/var/run/docker.sock:/var/run/docker.sock:ro" + + diff --git a/examples/docker-compose/odoo-and-db-ssl/traefik/certs/.gitignore b/examples/docker-compose/odoo-and-db-ssl/traefik/certs/.gitignore new file mode 100644 index 00000000..f2c4ee51 --- /dev/null +++ b/examples/docker-compose/odoo-and-db-ssl/traefik/certs/.gitignore @@ -0,0 +1 @@ +*.* diff --git a/examples/docker-compose/odoo-and-db-ssl/traefik/certs/README.md b/examples/docker-compose/odoo-and-db-ssl/traefik/certs/README.md new file mode 100644 index 00000000..eb160557 --- /dev/null +++ b/examples/docker-compose/odoo-and-db-ssl/traefik/certs/README.md @@ -0,0 +1,2 @@ +Place here certificates. +Name of certificate must be same as name of domain this sertificate is created for. diff --git a/examples/docker-compose/odoo-and-db-ssl/traefik/traefik-certs.yml b/examples/docker-compose/odoo-and-db-ssl/traefik/traefik-certs.yml new file mode 100644 index 00000000..2ec56ca0 --- /dev/null +++ b/examples/docker-compose/odoo-and-db-ssl/traefik/traefik-certs.yml @@ -0,0 +1,4 @@ +tls: + certificates: + - certFile: /certs/localhost.crt + keyFile: /certs/localhost.key diff --git a/examples/docker-compose/odoo-and-db/docker-compose.yml b/examples/docker-compose/odoo-and-db/docker-compose.yml new file mode 100644 index 00000000..bcc6ed0c --- /dev/null +++ b/examples/docker-compose/odoo-and-db/docker-compose.yml @@ -0,0 +1,63 @@ +version: '3' + +volumes: + odood-example-db-data: + odood-example-odoo-data: + +services: + odood-example-db: + image: postgres:15 + container_name: odood-example-db + environment: + - POSTGRES_USER=odoo + - POSTGRES_PASSWORD=odoo-db-pass + + # this is needed to avoid auto-creation of database by postgres itself + # databases must be created by Odoo only + - POSTGRES_DB=postgres + volumes: + - odood-example-db-data:/var/lib/postgresql/data + restart: "no" + + odood-example-odoo: + image: ghcr.io/katyukha/odood/odoo/17.0:latest + container_name: odood-example-odoo + labels: + - "traefik.enable=true" + - "traefik.http.routers.odoo-route.rule=Host(`localhost`)" + - "traefik.http.routers.odoo-route.service=odoo-service" + - "traefik.http.routers.odoo-route.entrypoints=web" + - "traefik.http.services.odoo-service.loadbalancer.server.port=8069" + - "traefik.http.routers.odoo-ge-route.rule=Host(`localhost`) && Path(`/websocket`)" + - "traefik.http.routers.odoo-ge-route.service=odoo-ge-service" + - "traefik.http.routers.odoo-ge-route.entrypoints=web" + - "traefik.http.services.odoo-ge-service.loadbalancer.server.port=8072" + + depends_on: + - odood-example-db + environment: + ODOOD_OPT_DB_HOST: odood-example-db + ODOOD_OPT_DB_USER: odoo + ODOOD_OPT_DB_PASSWORD: odoo-db-pass + ODOOD_OPT_ADMIN_PASSWD: admin + ODOOD_OPT_WORKERS: "1" + ODOOD_OPT_PROXY_MODE: True + volumes: + - odood-example-odoo-data:/opt/odoo/data + restart: "no" + + odood-example-traefik: + image: "traefik:v3.2" + container_name: "odood-example-traefik" + command: + #- "--log.level=DEBUG" + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.web.address=:80" + ports: + - "80:80" + - "8080:8080" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + diff --git a/subpackages/cli/dub.sdl b/subpackages/cli/dub.sdl index 90b674e3..86c7ffd3 100644 --- a/subpackages/cli/dub.sdl +++ b/subpackages/cli/dub.sdl @@ -5,9 +5,9 @@ copyright "Copyright © 2022-2023, Dmytro Katyukha" license "MPL-2.0" dependency "colored" version=">=0.0.31" dependency "tabletool" version="~>0.5.0" -/*dependency "commandr" version=">=1.0.0"*/ +dependency "commandr" version=">=1.1.0" /*dependency "commandr" path="../../../commandr"*/ -dependency "commandr" repository="git+https://github.com/katyukha/commandr.git" version="~master" +/*dependency "commandr" repository="git+https://github.com/katyukha/commandr.git" version="~master"*/ dependency "odood:lib" path="../../" diff --git a/subpackages/cli/dub.selections.json b/subpackages/cli/dub.selections.json index 7b505da4..3471a42f 100644 --- a/subpackages/cli/dub.selections.json +++ b/subpackages/cli/dub.selections.json @@ -5,7 +5,7 @@ "bindbc-loader": "1.1.5", "cachetools": "0.3.1", "colored": "0.0.31", - "commandr": {"version":"~master","repository":"git+https://github.com/katyukha/commandr.git"}, + "commandr": "1.1.0", "dini": "2.0.0", "dpq": "0.11.6", "dshould": "1.7.2", @@ -15,8 +15,8 @@ "prettyprint": "1.0.9", "requests": "2.1.3", "tabletool": "0.5.0", - "thepath": "1.2.0", - "theprocess": "0.0.5", + "thepath": "2.0.0", + "theprocess": "0.0.7", "tinyendian": "0.2.0", "unit-threaded": "2.1.9", "zipper": "0.0.5" diff --git a/subpackages/cli/source/odood/cli/app.d b/subpackages/cli/source/odood/cli/app.d index 15578b0f..48bb9a4f 100644 --- a/subpackages/cli/source/odood/cli/app.d +++ b/subpackages/cli/source/odood/cli/app.d @@ -11,6 +11,7 @@ private import odood.exception: OdoodException; private import odood.cli.core.logger: OdoodLogger; private import odood.cli.core: OdoodProgram, OdoodCommand; private import odood.cli.commands.init: CommandInit; +private import odood.cli.commands.deploy: CommandDeploy; private import odood.cli.commands.server: CommandServer, CommandServerStart, CommandServerStop, CommandServerRestart, CommandServerBrowse, CommandServerLogView; @@ -27,6 +28,7 @@ private import odood.cli.commands.script: CommandScript; private import odood.cli.commands.psql: CommandPSQL; private import odood.cli.commands.info: CommandInfo; private import odood.cli.commands.odoo: CommandOdoo; +private import odood.cli.commands.precommit: CommandPreCommit; /** Class that represents main OdoodProgram @@ -40,6 +42,7 @@ class App: OdoodProgram { this.summary("Easily manage odoo installations."); this.topicGroup("Main"); this.add(new CommandInit()); + this.add(new CommandDeploy()); this.add(new CommandServer()); this.add(new CommandStatus()); this.add(new CommandDatabase()); @@ -47,9 +50,13 @@ class App: OdoodProgram { this.add(new CommandTest()); this.add(new CommandRepository()); this.add(new CommandVenv()); + this.add(new CommandOdoo()); + + // Dev tools + this.topicGroup("Dev Tools"); this.add(new CommandScript()); this.add(new CommandPSQL()); - this.add(new CommandOdoo()); + this.add(new CommandPreCommit()); // System this.topicGroup("System"); diff --git a/subpackages/cli/source/odood/cli/commands/addons.d b/subpackages/cli/source/odood/cli/commands/addons.d index 4946e67c..44132374 100644 --- a/subpackages/cli/source/odood/cli/commands/addons.d +++ b/subpackages/cli/source/odood/cli/commands/addons.d @@ -20,7 +20,7 @@ private import odood.lib.project: Project; private import odood.utils.odoo.serie: OdooSerie; private import odood.utils.addons.addon: OdooAddon; private import odood.lib.odoo.log: OdooLogProcessor; -private import odood.lib.server.exception: ServerCommandFailedException; +private import odood.lib.addons.manager: AddonsInstallUpdateException; /** This exception could be throwed when install/update/uninstall command @@ -489,7 +489,7 @@ class CommandAddonsUpdateInstallUninstall: OdoodCommand { try { // Try to apply delegate dg(db); - } catch (ServerCommandFailedException e) { + } catch (AddonsInstallUpdateException e) { error = true; if (!log_file.isOpen && project.odoo.logfile.exists) @@ -583,12 +583,27 @@ class CommandAddonsUninstall: CommandAddonsUpdateInstallUninstall { } +/* TODO: implement command 'autoupdate' with following logic: + * 1. Create mapping {addon_name: version} for all addons available in + * filesystem. + * 2. For each database, check if there are addons on disk that are not + * mentioned in database. If such addons found, + * update list of addons in database. This stage ma fail and its ok. + * This stage needed to ensure that dependencies of modules updated. + * 3. for each database find installed addons that have different + * versions then in addons on disk. And update them + * + * This command should be useful on servers to automatically update server + * if needed. But this will require control over module versions. + */ + + class CommandAddonsAdd: OdoodCommand { this() { super("add", "Add addons to the project"); this.add(new Flag( null, "single-branch", - "Clone repository wihth --single-branch options. " ~ + "Clone repository with --single-branch options. " ~ "This could significantly reduce size of data to be downloaded " ~ "and increase performance.")); this.add(new Flag( diff --git a/subpackages/cli/source/odood/cli/commands/deploy.d b/subpackages/cli/source/odood/cli/commands/deploy.d new file mode 100644 index 00000000..7cb17e32 --- /dev/null +++ b/subpackages/cli/source/odood/cli/commands/deploy.d @@ -0,0 +1,151 @@ +module odood.cli.commands.deploy; + +private import core.sys.posix.unistd: geteuid, getegid; + +private import std.logger; +private import std.format: format; +private import std.exception: enforce, errnoEnforce; +private import std.conv: octal; +private import std.range: empty; + +private import thepath: Path; +private import theprocess: Process; +private import commandr: Option, Flag, ProgramArgs, acceptsValues; +private import dini: Ini; + +private import odood.cli.core: OdoodCommand, OdoodCLIException; +private import odood.lib.project: + Project, OdooInstallType; +private import odood.lib.project.config: ProjectServerSupervisor; +private import odood.lib.odoo.config: initOdooConfig; +private import odood.utils.odoo.serie: OdooSerie; +private import odood.utils: generateRandomString; + +private import odood.lib.deploy; + + +class CommandDeploy: OdoodCommand { + this() { + super("deploy", "Deploy production-ready Odoo."); + this.add(new Option("v", "odoo-version", "Version of Odoo to install") + .required().defaultValue("17.0")); + this.add(new Option( + null, "py-version", "Install specific python version.") + .defaultValue("auto")); + this.add(new Option( + null, "node-version", "Install specific node version.") + .defaultValue("lts")); + + this.add(new Option( + null, "db-host", "Database host").defaultValue("localhost")); + this.add(new Option( + null, "db-port", "Database port").defaultValue("False")); + this.add(new Option( + null, "db-user", "Database port").defaultValue("odoo")); + this.add(new Option( + null, "db-password", "Database password").defaultValue("odoo")); + this.add(new Flag( + null, "local-postgres", "Configure local postgresql server")); + + this.add(new Flag( + null, "proxy-mode", "Enable proxy-mode in odoo config")); + + this.add(new Flag( + null, "enable-logrotate", "Enable logrotate for Odoo.")); + + this.add(new Option( + null, "supervisor", "What superwisor to use for deployment.") + .defaultValue("systemd") + .acceptsValues(["odood", "init-script", "systemd"])); + + this.add(new Flag( + null, "log-to-stderr", "Log to stderr. Useful when running inside docker.")); + } + + DeployConfig parseDeployOptions(ProgramArgs args) { + DeployConfig config; + config.odoo.serie = OdooSerie(args.option("odoo-version")); + + if (args.option("py-version")) + config.py_version = args.option("py-version"); + if (args.option("node-version")) + config.node_version = args.option("node-version"); + + if (args.option("db-host")) + config.database.host = args.option("db-host"); + if (args.option("db-port")) + config.database.port = args.option("db-port"); + if (args.option("db-user")) + config.database.user = args.option("db-user"); + if (args.option("db-password")) + config.database.password = args.option("db-password"); + + if (args.flag("proxy-mode")) + config.odoo.proxy_mode = true; + + if (args.flag("local-postgres")) + config.database.local_postgres = true; + + if (config.database.local_postgres && config.database.password.empty) + /* Generate default password. + * Here we assume that new user will be created in local postgres. + * Most likely case. + */ + config.database.password = generateRandomString( + DEFAULT_PASSWORD_LEN); + + if (args.flag("enable-logrotate")) + config.logrotate_enable = true; + + if (args.option("supervisor")) + switch(args.option("supervisor")) { + case "odood": + config.odoo.server_supervisor = ProjectServerSupervisor.Odood; + break; + case "init-script": + config.odoo.server_supervisor = ProjectServerSupervisor.InitScript; + break; + case "systemd": + config.odoo.server_supervisor = ProjectServerSupervisor.Systemd; + break; + default: + assert(0, "Not supported supervisor"); + } + + if (args.flag("log-to-stderr")) + config.odoo.log_to_stderr = true; + + return config; + } + + void validateDeploy(ProgramArgs args, in DeployConfig deploy_config) { + // TODO: move to odood.lib.deploy + enforce!OdoodCLIException( + geteuid == 0 && getegid == 0, + "This command must be ran as root!"); + deploy_config.ensureValid(); + } + + public override void execute(ProgramArgs args) { + // TODO: run only as root + /* Plan: + * 0. Update locales + * 1. Deploy Odoo in /opt/odoo + * 2. Create system user + * 3. Set access rights for Odoo + * 4. Prepare systemd configuration for Odoo + * 5. Install postgres if needed + * 6. Create postgres user if needed + * 7. Configure logrotate + * 8. Install nginx if needed + * 9. Configure Odoo + */ + auto deploy_config = parseDeployOptions(args); + validateDeploy(args, deploy_config); + + auto project = deployOdoo(deploy_config); + } + +} + + diff --git a/subpackages/cli/source/odood/cli/commands/init.d b/subpackages/cli/source/odood/cli/commands/init.d index 00216646..a9282829 100644 --- a/subpackages/cli/source/odood/cli/commands/init.d +++ b/subpackages/cli/source/odood/cli/commands/init.d @@ -20,7 +20,7 @@ class CommandInit: OdoodCommand { this.add(new Option("i", "install-dir", "Directory to install odoo to") .required()); this.add(new Option("v", "odoo-version", "Version of Odoo to install") - .required().defaultValue("14.0")); + .required().defaultValue("17.0")); this.add(new Option( null, "install-type", "Installation type. Accept values: git, archive. Default: archive.") .defaultValue("archive") diff --git a/subpackages/cli/source/odood/cli/commands/precommit.d b/subpackages/cli/source/odood/cli/commands/precommit.d new file mode 100644 index 00000000..792b8488 --- /dev/null +++ b/subpackages/cli/source/odood/cli/commands/precommit.d @@ -0,0 +1,85 @@ +module odood.cli.commands.precommit; + +private import commandr: Argument, Option, Flag, ProgramArgs; + +private import thepath: Path; + +private import odood.cli.core: OdoodCommand; +private import odood.lib.project: Project; +private import odood.lib.devtools.precommit: + initPreCommit, + setUpPreCommit; + + +class CommandPreCommitInit: OdoodCommand { + this() { + super("init", "Initialize pre-commit for this repo."); + this.add(new Flag( + "f", "force", + "Enforce initialization. " ~ + "This will rewrite pre-commit configurations.")), + this.add(new Flag( + null, "no-setup", + "Do not set up pre-commit. " ~ + "Could be used if pre-commit already set up.")), + this.add(new Argument( + "path", "Path to repository to initialize pre-commit.").optional()); + } + + public override void execute(ProgramArgs args) { + auto project = Project.loadProject; + + auto repo = project.addons.getRepo( + args.arg("path") ? Path(args.arg("path")) : Path.current); + + repo.initPreCommit(args.flag("force"), !args.flag("no-setup")); + } + +} + +class CommandPreCommitSetUp: OdoodCommand { + this() { + super("set-up", "Set up pre-commit for specified repo."); + this.add(new Argument( + "path", "Path to repository to configure.").optional()); + } + + public override void execute(ProgramArgs args) { + auto project = Project.loadProject; + + auto repo = project.addons.getRepo( + args.arg("path") ? Path(args.arg("path")) : Path.current); + + repo.setUpPreCommit(); + } + +} + + +class CommandPreCommitRun: OdoodCommand { + this() { + super("run", "Run pre-commit for specified repo."); + this.add(new Argument( + "path", "Path to repository to run pre-commit for.").optional()); + } + + public override void execute(ProgramArgs args) { + auto project = Project.loadProject; + auto path = args.arg("path") ? Path(args.arg("path")) : Path.current; + project.venv.runner + .withArgs("pre-commit", "run", "--all-files") + .inWorkDir(path) + .execv(); + } + +} + + +class CommandPreCommit: OdoodCommand { + this() { + super("pre-commit", "Work with pre-commit dev tool."); + this.add(new CommandPreCommitInit()); + this.add(new CommandPreCommitSetUp()); + this.add(new CommandPreCommitRun()); + } +} diff --git a/subpackages/cli/source/odood/cli/commands/repository.d b/subpackages/cli/source/odood/cli/commands/repository.d index df2a5ff4..f1b8d386 100644 --- a/subpackages/cli/source/odood/cli/commands/repository.d +++ b/subpackages/cli/source/odood/cli/commands/repository.d @@ -8,7 +8,7 @@ private import thepath: Path; private import odood.cli.core: OdoodCommand; private import odood.lib.project: Project; -private import odood.lib.odoo.utils: fixVersionConflict; +private import odood.lib.odoo.utils: fixVersionConflict, updateManifestSerie; class CommandRepositoryAdd: OdoodCommand { @@ -88,37 +88,13 @@ class CommandRepositoryFixVersionConflict: OdoodCommand { } -class CommandRepositoryInitPreCommit: OdoodCommand { +class CommandRepositoryFixSerie: OdoodCommand { this() { - super("init-pre-commit", "Initialize pre-commit for this repo."); - this.add(new Flag( - "f", "force", - "Enforce initialization. " ~ - "This will rewrite pre-commit configurations.")), - this.add(new Flag( - null, "no-setup", - "Do not set up pre-commit. " ~ - "Could be used if pre-commit already set up.")), - this.add(new Argument( - "path", "Path to repository to initialize pre-commit.").optional()); - } - - public override void execute(ProgramArgs args) { - auto project = Project.loadProject; - - auto repo = project.addons.getRepo( - args.arg("path") ? Path(args.arg("path")) : Path.current); - - repo.initPreCommit(args.flag("force"), !args.flag("no-setup")); - } - -} - -class CommandRepositorySetUpPreCommit: OdoodCommand { - this() { - super("set-up-pre-commit", "Set up pre-commit for specified repo."); + super( + "fix-series", + "Fix series in manifests of addons in this repo. Set series to project's serie"); this.add(new Argument( - "path", "Path to repository to configure.").optional()); + "path", "Path to repository to fix conflicts in.").optional()); } public override void execute(ProgramArgs args) { @@ -126,29 +102,10 @@ class CommandRepositorySetUpPreCommit: OdoodCommand { auto repo = project.addons.getRepo( args.arg("path") ? Path(args.arg("path")) : Path.current); - - repo.setUpPreCommit(); - } - -} - - -class CommandRepositoryRunPreCommit: OdoodCommand { - this() { - super("run-pre-commit", "Run pre-commit for specified repo."); - this.add(new Argument( - "path", "Path to repository to run pre-commit for.").optional()); - } - - public override void execute(ProgramArgs args) { - auto project = Project.loadProject; - auto path = args.arg("path") ? Path(args.arg("path")) : Path.current; - project.venv.runner - .withArgs("pre-commit", "run", "--all-files") - .inWorkDir(path) - .execv(); + foreach(addon; repo.addons) + addon.path.join("__manifest__.py").updateManifestSerie( + project.odoo.serie); } - } @@ -157,9 +114,7 @@ class CommandRepository: OdoodCommand { super("repo", "Manage git repositories."); this.add(new CommandRepositoryAdd()); this.add(new CommandRepositoryFixVersionConflict()); - this.add(new CommandRepositoryInitPreCommit()); - this.add(new CommandRepositorySetUpPreCommit()); - this.add(new CommandRepositoryRunPreCommit()); + this.add(new CommandRepositoryFixSerie()); } } diff --git a/subpackages/cli/source/odood/cli/commands/server.d b/subpackages/cli/source/odood/cli/commands/server.d index 70296269..fa12a349 100644 --- a/subpackages/cli/source/odood/cli/commands/server.d +++ b/subpackages/cli/source/odood/cli/commands/server.d @@ -5,12 +5,13 @@ private import std.logger; private import std.conv: to; private import std.format: format; private import std.exception: enforce; +private import std.algorithm.searching: canFind, startsWith; private import thepath: Path; private import theprocess: Process; private import commandr: Option, Flag, ProgramArgs; -private import odood.cli.core: OdoodCommand; +private import odood.cli.core: OdoodCommand, OdoodCLIException; private import odood.lib.project: Project; private import odood.utils.odoo.serie: OdooSerie; @@ -18,14 +19,45 @@ private import odood.utils.odoo.serie: OdooSerie; class CommandServerRun: OdoodCommand { this() { super("run", "Run the server."); - this.add(new Flag("d", "detach", "Run the server in background.")); + this.add(new Flag( + null, "ignore-running", "Ingore running Odoo instance. (Do not check/create pidfile).")); + + version(OdoodInDocker) + this.add(new Flag( + null, "config-from-env", "Apply odoo configuration from envrionment")); } public override void execute(ProgramArgs args) { auto project = Project.loadProject; - project.server.spawn(args.flag("detach")); - } + auto runner = project.server.getServerRunner(); + + if (args.flag("ignore-running")) { + // if no --pidfile option specified, enforce no pidfile. + // This is needed to avoid messing up pid of running app. + if (!args.argsRest.canFind!((e) => e.startsWith("--pidfile"))) + runner.addArgs("--pidfile="); + } else { + enforce!OdoodCLIException( + !project.server.isRunning, + "Odoo server already running!"); + } + + version(OdoodInDocker) if (args.flag("config-from-env")) { + import std.process: environment; + import std.string: chompPrefix, toLower; + auto config = project.getOdooConfig; + foreach(kv; environment.toAA.byKeyValue) { + string key = kv.key.toLower.chompPrefix("odood_opt_"); + config["options"].setKey(key, kv.value); + } + // In case when we running in Docker, we can just rewrite config + config.save(project.odoo.configfile.toString); + } + runner.addArgs(args.argsRest); + debug tracef("Running Odoo: %s", runner); + runner.execv; + } } diff --git a/subpackages/git/.gitignore b/subpackages/git/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/subpackages/git/.gitignore @@ -0,0 +1 @@ +/build diff --git a/subpackages/git/dub.sdl b/subpackages/git/dub.sdl new file mode 100644 index 00000000..884e6086 --- /dev/null +++ b/subpackages/git/dub.sdl @@ -0,0 +1,25 @@ +name "git" +description "Odood wrapper for Git CLI" +authors "Dmytro Katyukha" +copyright "Copyright © 2022-2023, Dmytro Katyukha" +license "MPL-2.0" + +dependency "thepath" version=">=1.2.0" +dependency "theprocess" version=">=0.0.7" + +targetPath "build" +targetType "library" + +dependency "odood:exception" path="../../" + +configuration "library" { +} + +configuration "sourceLibrary" { + targetType "sourceLibrary" +} + +configuration "unittest" { + dependency "unit-threaded:assertions" version=">=2.0.0" +} + diff --git a/subpackages/git/dub.selections.json b/subpackages/git/dub.selections.json new file mode 100644 index 00000000..1e2fb497 --- /dev/null +++ b/subpackages/git/dub.selections.json @@ -0,0 +1,11 @@ +{ + "fileVersion": 1, + "versions": { + "dshould": "1.7.2", + "odood": {"path":"../../"}, + "prettyprint": "1.0.9", + "thepath": "2.0.0", + "theprocess": "0.0.7", + "unit-threaded": "2.2.0" + } +} diff --git a/subpackages/utils/source/odood/utils/git/package.d b/subpackages/git/source/odood/git/package.d similarity index 72% rename from subpackages/utils/source/odood/utils/git/package.d rename to subpackages/git/source/odood/git/package.d index 8c820c3b..9af071a4 100644 --- a/subpackages/utils/source/odood/utils/git/package.d +++ b/subpackages/git/source/odood/git/package.d @@ -1,16 +1,17 @@ -module odood.utils.git; +module odood.git; private import std.logger: infof; private import std.exception: enforce; private import std.format: format; +private import std.string: strip; private import thepath: Path; private import odood.exception: OdoodException; private import theprocess: Process; -public import odood.utils.git.url: GitURL; -public import odood.utils.git.repository: GitRepository; +public import odood.git.url: GitURL; +public import odood.git.repository: GitRepository; /// Parse git url for further processing @@ -19,7 +20,7 @@ GitURL parseGitURL(in string url) { } /// Clone git repository to provided destination directory -void gitClone( +GitRepository gitClone( in GitURL repo, in Path dest, in string branch, @@ -41,6 +42,7 @@ void gitClone( proc.addArgs("--single-branch"); proc.addArgs(repo.applyCIRewrites.toUrl, dest.toString); proc.execute().ensureOk(true); + return new GitRepository(dest); } @@ -59,3 +61,20 @@ bool isGitRepo(in Path path) { return false; } + +/** Returns absolute path to repository root directory. + + Parametrs: + path = any path inside repository + **/ +Path getGitTopLevel(in Path path) { + return Path( + Process("git") + .inWorkDir(path) + .withArgs("rev-parse", "--show-toplevel") + .execute + .ensureOk(true) + .output + .strip + ); +} diff --git a/subpackages/utils/source/odood/utils/git/repository.d b/subpackages/git/source/odood/git/repository.d similarity index 61% rename from subpackages/utils/source/odood/utils/git/repository.d rename to subpackages/git/source/odood/git/repository.d index d5976548..e5220ccc 100644 --- a/subpackages/utils/source/odood/utils/git/repository.d +++ b/subpackages/git/source/odood/git/repository.d @@ -1,4 +1,4 @@ -module odood.utils.git.repository; +module odood.git.repository; private import std.typecons: Nullable, nullable; private import std.string: chompPrefix, strip; @@ -8,6 +8,7 @@ private import thepath: Path; private import odood.exception: OdoodException; private import theprocess; +private import odood.git: getGitTopLevel; /** Simple class to manage git repositories @@ -18,13 +19,21 @@ class GitRepository { @disable this(); this(in Path path) { - // TODO: automatically handle root path for the repo? - _path = path; + if (path.join(".git").exists) + _path = path; + else + _path = getGitTopLevel(path); } /// Return path for this repo auto path() const => _path; + /// Preconfigured runner for git CLI + protected auto gitCmd() const { + return Process("git") + .inWorkDir(_path); + } + /** Find the name of current git branch for this repo. * * Returns: Nullable!string @@ -32,9 +41,8 @@ class GitRepository { * If result is null, then git repository is in detached-head mode. **/ Nullable!string getCurrBranch() { - auto result = Process("git") - .setArgs(["symbolic-ref", "-q", "HEAD"]) - .setWorkDir(_path) + auto result = gitCmd + .withArgs(["symbolic-ref", "-q", "HEAD"]) .setFlag(std.process.Config.Flags.stderrPassThrough) .execute(); if (result.status == 0) @@ -48,9 +56,8 @@ class GitRepository { * SHA1 hash of current commit **/ string getCurrCommit() { - return Process("git") - .setArgs(["rev-parse", "-q", "HEAD"]) - .setWorkDir(_path) + return gitCmd + .withArgs(["rev-parse", "-q", "HEAD"]) .setFlag(std.process.Config.stderrPassThrough) .execute() .ensureStatus(true) @@ -60,18 +67,16 @@ class GitRepository { /** Fetch remote 'origin' **/ void fetchOrigin() { - Process("git") - .setArgs("fetch", "origin") - .setWorkDir(_path) + gitCmd + .withArgs("fetch", "origin") .execute() .ensureStatus(true); } /// ditto void fetchOrigin(in string branch) { - Process("git") + gitCmd .setArgs("fetch", "origin", branch) - .setWorkDir(_path) .execute() .ensureStatus(true); } @@ -79,12 +84,35 @@ class GitRepository { /** Switch repo to specified branch **/ void switchBranchTo(in string branch_name) { - Process("git") + gitCmd .setArgs("checkout", branch_name) - .setWorkDir(_path) .execute() .ensureStatus(true); } + + /** Set annotation tag on current commit in repo + **/ + void setTag(in string tag_name, in string message = null) + in (tag_name.length > 0) { + // TODO: add ability to set tag on specific commit + gitCmd + .withArgs( + "tag", + "-a", tag_name, + "-m", message.length > 0 ? message : tag_name) + .execute() + .ensureOk(true); + } + + /** Pull repository + **/ + void pull() { + gitCmd + .withArgs("pull") + .execute() + .ensureOk(true); + } } + diff --git a/subpackages/utils/source/odood/utils/git/url.d b/subpackages/git/source/odood/git/url.d similarity index 99% rename from subpackages/utils/source/odood/utils/git/url.d rename to subpackages/git/source/odood/git/url.d index a7143c90..4b6201c6 100644 --- a/subpackages/utils/source/odood/utils/git/url.d +++ b/subpackages/git/source/odood/git/url.d @@ -1,4 +1,4 @@ -module odood.utils.git.url; +module odood.git.url; private import std.logger: infof; private import std.regex: ctRegex, matchFirst; @@ -12,7 +12,7 @@ private import thepath: Path; private import odood.exception: OdoodException; private import theprocess: Process; - +// TODO: Think about using https://code.dlang.org/packages/urld // TODO: Add parsing of branch name from url /// Regex for parsing git URL private auto immutable RE_GIT_URL = ctRegex!( @@ -261,3 +261,4 @@ unittest { toUrl.shouldEqual("ssh://git@gitlab.crnd.pro/crnd/crnd-account"); } } + diff --git a/subpackages/lib/dub.sdl b/subpackages/lib/dub.sdl index beec6eda..8d225894 100644 --- a/subpackages/lib/dub.sdl +++ b/subpackages/lib/dub.sdl @@ -12,6 +12,7 @@ dependency "dpq" version=">=0.11.6" targetPath "build" targetType "library" +dependency "odood:git" path="../../" dependency "odood:utils" path="../../" dependency "odood:exception" path="../../" diff --git a/subpackages/lib/dub.selections.json b/subpackages/lib/dub.selections.json index 7d18970e..c6ea85ee 100644 --- a/subpackages/lib/dub.selections.json +++ b/subpackages/lib/dub.selections.json @@ -11,8 +11,8 @@ "odood": {"path":"../../"}, "prettyprint": "1.0.9", "requests": "2.1.3", - "thepath": "1.2.0", - "theprocess": "0.0.5", + "thepath": "2.0.0", + "theprocess": "0.0.7", "tinyendian": "0.2.0", "unit-threaded": "2.2.0", "zipper": "0.0.5" diff --git a/subpackages/lib/source/odood/lib/addons/manager.d b/subpackages/lib/source/odood/lib/addons/manager.d index 5174ab57..4c370835 100644 --- a/subpackages/lib/source/odood/lib/addons/manager.d +++ b/subpackages/lib/source/odood/lib/addons/manager.d @@ -6,7 +6,7 @@ private import std.array: split, empty, array; private import std.string: join, strip, startsWith, toLower; private import std.format: format; private import std.file: SpanMode; -private import std.exception: enforce, ErrnoException; +private import std.exception: enforce, ErrnoException, basicExceptionCtors; private import std.algorithm: map, canFind; private import thepath: Path, createTempPath; @@ -20,7 +20,7 @@ private import odood.utils.addons.odoo_requirements: parseOdooRequirements, OdooRequirementsLineType; private import odood.lib.addons.repository: AddonRepository; private import odood.utils: download; -private import odood.utils.git: parseGitURL, gitClone; +private import odood.git: parseGitURL, gitClone; private import odood.exception: OdoodException; /// Install python dependencies requirements.txt by default @@ -29,6 +29,18 @@ immutable bool DEFAULT_INSTALL_PY_REQUREMENTS = true; /// Install python dependencies from addon manifest by default immutable bool DEFAULT_INSTALL_MANIFEST_REQUREMENTS = false; +class AddonsInstallUpdateException: OdoodException { + mixin basicExceptionCtors; +} + +class AddonsInstallException : AddonsInstallUpdateException { + mixin basicExceptionCtors; +} + +class AddonsUpdateException : AddonsInstallUpdateException { + mixin basicExceptionCtors; +} + /// Struct that provide API to manage odoo addons for the project struct AddonManager { @@ -308,30 +320,30 @@ struct AddonManager { in cmdIU cmd, in string[string] env=null) const { - string[] server_opts=[ + // Initialize server runner configuration + auto runner = _project.server.getServerRunner( "-d", database, "--max-cron-threads=0", "--stop-after-init", _project.odoo.serie <= OdooSerie(10) ? "--no-xmlrpc" : "--no-http", "--pidfile=", // We must not write to pidfile to avoid conflicts with running Odoo "--logfile=%s".format(_project.odoo.logfile.toString), - ]; - + ).withEnv(env); if (!_project.hasDatabaseDemoData(database)) - server_opts ~= ["--without-demo=all"]; + runner.addArgs("--without-demo=all"); auto addon_names_csv = addon_names.join(","); final switch(cmd) { case cmdIU.install: infof("Installing addons (db=%s): %s", database, addon_names_csv); - _project.server(_test_mode).runE( - server_opts ~ ["--init=%s".format(addon_names_csv)], env); + runner.addArgs("--init=%s".format(addon_names_csv)) + .execute.ensureOk!AddonsInstallException(true); infof("Installation of addons for database %s completed!", database); break; case cmdIU.update: infof("Updating addons (db=%s): %s", database, addon_names_csv); - _project.server(_test_mode).runE( - server_opts ~ ["--update=%s".format(addon_names_csv)], env); + runner.addArgs("--update=%s".format(addon_names_csv)) + .execute.ensureOk!AddonsUpdateException(true); infof("Update of addons for database %s completed!", database); break; case cmdIU.uninstall: @@ -596,10 +608,8 @@ struct AddonManager { return; } - gitClone(git_url, dest, branch, single_branch); + auto repo = gitClone(git_url, dest, branch, single_branch); - // TODO: Do we need to create instance of repo here? - auto repo = new AddonRepository(_project, dest); link( repo.path, true, // recursive diff --git a/subpackages/lib/source/odood/lib/addons/repository.d b/subpackages/lib/source/odood/lib/addons/repository.d index 482f6e5b..b9236fbe 100644 --- a/subpackages/lib/source/odood/lib/addons/repository.d +++ b/subpackages/lib/source/odood/lib/addons/repository.d @@ -1,30 +1,15 @@ module odood.lib.addons.repository; private import std.logger: warningf, infof; -private import std.exception: enforce; private import thepath: Path; private import odood.lib.project: Project; private import odood.exception: OdoodException; -private import odood.utils.git: GitRepository, parseGitURL, gitClone; -private import theprocess; - -// TODO: may be move pre-commit logic somewhere else? - -// Configuration files for pre-commit for Odoo version 17 -immutable string ODOO_PRE_COMMIT_17_PRECOMMIT = import( - "pre-commit/17.0/pre-commit-config.yaml"); -immutable string ODOO_PRE_COMMIT_17_ESLINT = import( - "pre-commit/17.0/eslintrc.yml"); -immutable string ODOO_PRE_COMMIT_17_FLAKE8 = import( - "pre-commit/17.0/flake8"); -immutable string ODOO_PRE_COMMIT_17_ISORT = import( - "pre-commit/17.0/isort.cfg"); -immutable string ODOO_PRE_COMMIT_17_PYLINT = import( - "pre-commit/17.0/pylintrc"); +private import odood.git: GitRepository; +// TODO: Do we need this class? class AddonRepository : GitRepository{ private const Project _project; @@ -35,53 +20,23 @@ class AddonRepository : GitRepository{ _project = project; } - auto project() const => _project; - - /// Check if repository has pre-commit configuration. - bool hasPreCommitConfig() const { - return path.join(".pre-commit-config.yaml").exists; - } - - /// Initialize pre-commit for this repository - void initPreCommit(in bool force=false, in bool setup=true) const { - enforce!OdoodException( - force || !hasPreCommitConfig, - "Cannot init pre-commit. Configuration already exists"); - enforce!OdoodException( - project.odoo.serie == 17, - "This feature is available only for Odoo 17 at the moment!"); - infof("Initializing pre-commit for %s repo!", path); - this.path.join(".pre-commit-config.yaml").writeFile( - ODOO_PRE_COMMIT_17_PRECOMMIT); - this.path.join(".eslintrc.yml").writeFile( - ODOO_PRE_COMMIT_17_ESLINT); - this.path.join(".flake8").writeFile( - ODOO_PRE_COMMIT_17_FLAKE8); - this.path.join(".isort.cfg").writeFile( - ODOO_PRE_COMMIT_17_ISORT); - this.path.join(".pylintrc").writeFile( - ODOO_PRE_COMMIT_17_PYLINT); - - if (setup) - setUpPreCommit(); + this(in Project project, in GitRepository repo) { + super(repo.path); + _project = project; } - /// Setup Precommit if needed - void setUpPreCommit() const { - if (hasPreCommitConfig) { - infof("Setting up pre-commit for %s repo!", path); - _project.venv.installPyPackages("pre-commit"); - _project.venv.runE(["pre-commit", "install"]); - } else { - warningf( - "Cannot set up pre-commit for repository %s, " ~ - "because it does not have pre-commit configuration!", - path); - } - } + /** Return Odood project associated with this addons repository + **/ + auto project() const => _project; - /// Return array of odoo addons, found in this repo. - /// this method searches for addons recursively by default. + /** Scan repository for addons and return array of odoo addons, + * found in this repo. + * This method searches for addons recursively by default. + * + * Params: + * recursive = If set to true, search for addons recursively inside repo. + * Otherwise, scan only the root directory of the repo for addons. + **/ auto addons(in bool recursive=true) const { return project.addons.scan(path, recursive); } diff --git a/subpackages/lib/source/odood/lib/deploy/config.d b/subpackages/lib/source/odood/lib/deploy/config.d new file mode 100644 index 00000000..47299e4e --- /dev/null +++ b/subpackages/lib/source/odood/lib/deploy/config.d @@ -0,0 +1,169 @@ +module odood.lib.deploy.config; + +private import std.conv: to; +private import std.range: empty; +private import std.exception: enforce; +private import std.format: format; + +private import thepath: Path; + +private import odood.lib.odoo.config: initOdooConfig; +private import odood.lib.project: + Project, + OdooInstallType, + ODOOD_SYSTEM_CONFIG_PATH; +private import odood.lib.project.config: + ProjectServerSupervisor, + ProjectConfigDirectories, + ProjectConfigOdoo; +private import odood.lib.deploy.exception: OdoodDeployException; +private import odood.lib.deploy.utils: dpkgCheckPackageInstalled; +private import odood.utils.odoo.serie: OdooSerie; +private import odood.utils: generateRandomString; + +immutable auto DEFAULT_PASSWORD_LEN = 32; + +struct DeployConfigDatabase { + string host="localhost"; + string port="5432"; + string user="odoo"; + string password; + bool local_postgres=false; +} + +struct DeployConfigOdoo { + OdooSerie serie; + bool proxy_mode=false; + string http_host=null; + string http_port="8069"; + uint workers=0; + + string server_user="odoo"; + ProjectServerSupervisor server_supervisor=ProjectServerSupervisor.Systemd; + Path server_init_script_path = Path( + "/", "etc", "init.d", "odoo"); + Path server_systemd_service_path = Path( + "/", "etc", "systemd", "system", "odoo.service"); + + Path pidfile = Path("/", "var", "run", "odoo.pid"); + + bool log_to_stderr = false; +} + +struct DeployConfig { + Path deploy_path = Path("/", "opt", "odoo"); + string py_version="auto"; + string node_version="lts"; + OdooInstallType install_type=OdooInstallType.Archive; + + DeployConfigDatabase database; + DeployConfigOdoo odoo; + + bool logrotate_enable = false; + Path logrotate_config_path = Path("/", "etc", "logrotate.d", "odoo"); + + /** Validate deploy config + * Throw exception if config is not valid. + **/ + void ensureValid() const { + enforce!OdoodDeployException( + this.odoo.serie.isValid, + "Odoo version is not valid"); + enforce!OdoodDeployException( + !this.deploy_path.exists, + "Deploy path %s already exists. ".format(this.deploy_path) ~ + "It seems that there was attempt to install Odoo. " ~ + "This command can install Odoo only on clean machine."); + enforce!OdoodDeployException( + !ODOOD_SYSTEM_CONFIG_PATH.exists, + "Odood system-wide config already exists at %s. ".format(ODOOD_SYSTEM_CONFIG_PATH) ~ + "It seems that there was attempt to install Odoo. " ~ + "This command can install Odoo only on clean machine."); + + if (this.logrotate_enable) + enforce!OdoodDeployException( + !Path("etc", "logrotate.d", "odoo").exists, + "It seems that Odoo config for logrotate already exists!"); + + final switch(this.odoo.server_supervisor) { + case ProjectServerSupervisor.Odood: + // Do nothing, no additional check needed. + break; + case ProjectServerSupervisor.InitScript: + enforce!OdoodDeployException( + !this.odoo.server_init_script_path.exists, + "It seems that init.d script for Odoo already exists!"); + break; + case ProjectServerSupervisor.Systemd: + enforce!OdoodDeployException( + !this.odoo.server_systemd_service_path.exists, + "It seems that systemd service for Odoo already exists!"); + break; + } + + enforce!OdoodDeployException( + !this.database.password.empty, + "Password for database must not be empty!"); + + if (this.database.local_postgres) + enforce!OdoodDeployException( + !dpkgCheckPackageInstalled("potgresql"), + "Local postgres requested, but 'postgresql' package is not installed!"); + } + + /** Prepare odoo configuration file for this deployment + * based on this deployment configuration + **/ + auto prepareOdooConfig(in Project project) const + in ( + project.odoo.serie == this.odoo.serie + ) { + auto odoo_config = initOdooConfig(project); + odoo_config["options"].setKey( + "admin_passwd", generateRandomString(DEFAULT_PASSWORD_LEN)); + + // DB config + odoo_config["options"].setKey("db_host", database.host); + odoo_config["options"].setKey("db_port", database.port); + odoo_config["options"].setKey("db_user", database.user); + odoo_config["options"].setKey("db_password", database.password); + + if (odoo.serie < OdooSerie(11)) { + if (odoo.http_host.length > 0) + odoo_config["options"].setKey("xmlrpc_interface", odoo.http_host); + odoo_config["options"].setKey("xmlrpc_port", odoo.http_port); + } else { + if (odoo.http_host.length > 0) + odoo_config["options"].setKey("http_interface", odoo.http_host); + odoo_config["options"].setKey("http_port", odoo.http_port); + } + + odoo_config["options"].setKey("workers", odoo.workers.to!string); + + if (odoo.proxy_mode) + odoo_config["options"].setKey("proxy_mode", "True"); + + if (odoo.log_to_stderr) + odoo_config["options"].removeKey("logfile"); + + return odoo_config; + } + + /** Prepare Odood project for deployment + **/ + auto prepareOdoodProject() const { + auto project_directories = ProjectConfigDirectories(this.deploy_path); + auto project_odoo = ProjectConfigOdoo( + this.deploy_path, project_directories, this.odoo.serie); + project_odoo.server_user = this.odoo.server_user; + project_odoo.server_supervisor = this.odoo.server_supervisor; + project_odoo.server_systemd_service_path = this.odoo.server_systemd_service_path; + project_odoo.server_init_script_path = this.odoo.server_init_script_path; + project_odoo.pidfile = this.odoo.pidfile; + + return new Project( + this.deploy_path, + project_directories, + project_odoo); + } +} diff --git a/subpackages/lib/source/odood/lib/deploy/exception.d b/subpackages/lib/source/odood/lib/deploy/exception.d new file mode 100644 index 00000000..f819d45d --- /dev/null +++ b/subpackages/lib/source/odood/lib/deploy/exception.d @@ -0,0 +1,11 @@ +module odood.lib.deploy.exception; + +private import std.exception: basicExceptionCtors; + +private import odood.exception: OdoodException; + + +class OdoodDeployException : OdoodException { + mixin basicExceptionCtors; +} + diff --git a/subpackages/lib/source/odood/lib/deploy/odoo.d b/subpackages/lib/source/odood/lib/deploy/odoo.d new file mode 100644 index 00000000..a2b842df --- /dev/null +++ b/subpackages/lib/source/odood/lib/deploy/odoo.d @@ -0,0 +1,260 @@ +module odood.lib.deploy.odoo; + +private import core.sys.posix.unistd: geteuid, getegid; +private import core.sys.posix.pwd: getpwnam, passwd; + +private import std.logger: infof; +private import std.exception: enforce, errnoEnforce; +private import std.conv: to, text, octal; +private import std.format: format; +private import std.string: toStringz; + +private import thepath: Path; +private import theprocess: Process; + +private import odood.utils.odoo.serie: OdooSerie; +private import odood.lib.project: Project, ODOOD_SYSTEM_CONFIG_PATH; +private import odood.lib.project.config: ProjectServerSupervisor; + +private import odood.lib.deploy.config: DeployConfig; +private import odood.lib.deploy.utils: + checkSystemUserExists, + createSystemUser, + postgresCheckUserExists, + postgresCreateUser; + + +private void deployInitScript(in Project project) { + + infof("Configuring init script for Odoo..."); + + // Configure systemd + project.odoo.server_init_script_path.writeFile( +i"#!/bin/bash +### BEGIN INIT INFO +# Provides: odoo +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start odoo daemon at boot time +# Description: Enable service provided by daemon. +# X-Interactive: true +### END INIT INFO +## more info: http://wiki.debian.org/LSBInitScripts + +. /lib/lsb/init-functions + +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin:$(project.venv.bin_path.toString) +DAEMON=$(project.server.scriptPath.toString) + NAME=odoo + DESC=odoo + CONFIG=$(project.odoo.configfile.toString) +LOGFILE=$(project.odoo.logfile.toString) +PIDFILE=$(project.odoo.pidfile.toString).pid +USER=$(project.odoo.server_user) +export LOGNAME=$USER + +test -x $DAEMON || exit 0 +set -e + +function _start() { + start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE +} + +function _stop() { + start-stop-daemon --stop --quiet --pidfile $PIDFILE --oknodo --retry 3 + rm -f $PIDFILE +} + +function _status() { + start-stop-daemon --status --quiet --pidfile $PIDFILE + return $? +} + + +case \"$1\" in + start) + echo -n \"Starting $DESC: \" + _start + echo \"ok\" + ;; + stop) + echo -n \"Stopping $DESC: \" + _stop + echo \"ok\" + ;; + restart|force-reload) + echo -n \"Restarting $DESC: \" + _stop + sleep 1 + _start + echo \"ok\" + ;; + status) + echo -n \"Status of $DESC: \" + _status && echo \"running\" || echo \"stopped\" + ;; + *) + N=/etc/init.d/$NAME + echo \"Usage: $N {start|stop|restart|force-reload|status}\" >&2 + exit 1 + ;; +esac + +exit 0 +".text); + + // Set access rights for systemd config + project.odoo.server_init_script_path.setAttributes(octal!755); + project.odoo.server_init_script_path.chown("root", "root"); + + // Enable systemd service for Odoo + Process("update-rc.d") + .withArgs("odoo", "defaults") + .execute + .ensureOk(true); + infof("Init script configred successfully. Odoo will be started at startup."); +} + + +private void deploySystemdConfig(in Project project) { + + infof("Configuring systemd daemon for Odoo..."); + + // Configure systemd + project.odoo.server_systemd_service_path.writeFile( +i"[Unit] +Description=Odoo Open Source ERP and CRM +After=network.target + +[Service] +Type=simple +User=$(project.odoo.server_user) +Group=$(project.odoo.server_user) +ExecStart=$(project.server.scriptPath) --config $(project.odoo.configfile) +KillMode=mixed + +[Install] +WantedBy=multi-user.target +".text); + + // Set access rights for systemd config + project.odoo.server_systemd_service_path.setAttributes(octal!755); + project.odoo.server_systemd_service_path.chown("root", "root"); + + // Enable systemd service for Odoo + Process("systemctl") + .withArgs("daemon-reload") + .execute + .ensureOk(true); + Process("systemctl") + .withArgs("enable", "--now", "odoo.service") + .execute + .ensureOk(true); + Process("systemctl") + .withArgs("start", "odoo.service") + .execute + .ensureOk(true); + + infof("Systemd configred successfully. Odoo will be started at startup."); +} + + +private void deployLogrotateConfig(in Project project, in DeployConfig config) { + infof("Configuring logrotate for Odoo..."); + config.logrotate_config_path.writeFile( +i"$(project.directories.log.toString)/*.log { + copytruncate + missingok + notifempty +}".text); + + // Set access rights for logrotate config + config.logrotate_config_path.setAttributes(octal!755); + config.logrotate_config_path.chown("root", "root"); + + infof("Logrotate configured successfully."); +} + + +/** Deploy Odoo according provided DeployConfig + **/ +Project deployOdoo(in DeployConfig config) { + infof("Deploying Odoo %s to %s", config.odoo.serie, config.deploy_path); + + // TODO: Move this configuration to Deploy config + auto project = config.prepareOdoodProject(); + + // We need to keep reference on odoo_config to make initialize work. + // TODO: Fix this + auto odoo_config = config.prepareOdooConfig(project); + + // Initialize project. + project.initialize( + odoo_config, + config.py_version, + config.node_version, + config.install_type); + project.save(ODOOD_SYSTEM_CONFIG_PATH); + + if (!checkSystemUserExists(project.odoo.server_user)) + createSystemUser(project.project_root, project.odoo.server_user); + + // Get info about odoo user (that is needed to set up access rights for Odoo files + auto pw_odoo = getpwnam(project.odoo.server_user.toStringz); + errnoEnforce( + pw_odoo !is null, + "Cannot get info about user %s".format(project.odoo.server_user)); + + // Config is owned by root, but readable by Odoo + project.odoo.configfile.chown(0, pw_odoo.pw_gid); + project.odoo.configfile.setAttributes(octal!640); + + // Odoo can read and write and create files in log directory + project.directories.log.chown(pw_odoo.pw_uid, pw_odoo.pw_gid); + project.directories.log.setAttributes(octal!750); + + // Make Odoo owner of data directory. Do not allow others to access it. + project.directories.data.chown(pw_odoo.pw_uid, pw_odoo.pw_gid); + project.directories.data.setAttributes(octal!750); + + // Make Odoo owner of project root (/opt/odoo), but not recursively, + // thus, Odoo will be able to create files there, + // but will not be allowed to change existing files. + project.project_root.chown(pw_odoo.pw_uid, pw_odoo.pw_gid); + + // Create postgresql user if "local-postgres" is selected and no user exists + if (config.database.local_postgres) + /* In this case we need to create postgres user + * only if it does not exists yet. + * If user already exists, we expect, + * that user provided correct password for it. + */ + if (!postgresCheckUserExists(config.database.user)) + postgresCreateUser(config.database.user, config.database.password); + + // Configure logrotate + if (config.logrotate_enable) + deployLogrotateConfig(project, config); + + // Configure systemd + final switch(config.odoo.server_supervisor) { + case ProjectServerSupervisor.Odood: + // Do nothing. + // TODO: May be it have sense to create some link in /usr/sbin for Odoo? + break; + case ProjectServerSupervisor.InitScript: + deployInitScript(project); + break; + case ProjectServerSupervisor.Systemd: + deploySystemdConfig(project); + break; + } + + infof("Odoo deployed successfully."); + return project; +} + + + diff --git a/subpackages/lib/source/odood/lib/deploy/package.d b/subpackages/lib/source/odood/lib/deploy/package.d new file mode 100644 index 00000000..f0d4bfb9 --- /dev/null +++ b/subpackages/lib/source/odood/lib/deploy/package.d @@ -0,0 +1,4 @@ +module odood.lib.deploy; + +public import odood.lib.deploy.config: DeployConfig, DEFAULT_PASSWORD_LEN; +public import odood.lib.deploy.odoo: deployOdoo; diff --git a/subpackages/lib/source/odood/lib/deploy/utils.d b/subpackages/lib/source/odood/lib/deploy/utils.d new file mode 100644 index 00000000..8a683ae3 --- /dev/null +++ b/subpackages/lib/source/odood/lib/deploy/utils.d @@ -0,0 +1,90 @@ +module odood.lib.deploy.utils; + +private import std.logger: infof, tracef; +private import std.format: format; +private import std.exception: enforce, errnoEnforce; +private import std.conv: to, text; +private import std.string: strip; + +private import core.sys.posix.unistd: geteuid, getegid; +private import core.sys.posix.pwd: getpwnam_r, passwd; + +private import theprocess: Process; +private import thepath: Path; + + +bool checkSystemUserExists(in string username) { + import std.string: toStringz; + passwd pwd; + passwd* result; + long bufsize = 16384; + char[] buf = new char[bufsize]; + + int s = getpwnam_r(username.toStringz, &pwd, &buf[0], bufsize, &result); + errnoEnforce( + s == 0, + "Got error on attempt to check if user %s exists".format(username)); + if (result) + return true; + return false; +} + + +void createSystemUser(in Path home, in string name) { + Process("adduser") + .withArgs( + "--system", "--no-create-home", + "--home", home.toString, + "--quiet", + "--group", + name) + .execute() + .ensureOk(true); +} + + +/** Check if PostgreSQL user with provided username exists + **/ +bool postgresCheckUserExists(in string username) { + auto output = Process("psql") + .setArgs([ + "-t", "-A", "-c", + i"SELECT count(*) FROM pg_user WHERE usename = '$(username)';".text, + ]) + .withUser("postgres") + .execute + .ensureOk(true) + .output.strip; + + return output.to!int != 0; +} + + +/** Create new PostgreSQL user for Odoo with provided credentials + **/ +void postgresCreateUser(in string username, in string password) { + infof("Creating postgresql user '%s' for Odoo...", username); + Process("psql") + .setArgs([ + "-c", + i"CREATE USER \"$(username)\" WITH CREATEDB PASSWORD '$(password)'".text, + ]) + .withUser("postgres") + .execute + .ensureOk(true); + infof("Postgresql user '%s' for Odoo created successfully.", username); +} + + +/** Check if debian package is installed in system + **/ +bool dpkgCheckPackageInstalled(in string package_name) { + auto result = Process("dpkg-query") + .withArgs("--show", "--showformat='${db:Status-Status}'", package_name) + .execute; + if (result.isNotOk) + return false; + if (result.output == "installed") + return true; + return false; +} diff --git a/subpackages/lib/source/odood/lib/devtools/package.d b/subpackages/lib/source/odood/lib/devtools/package.d new file mode 100644 index 00000000..15a14665 --- /dev/null +++ b/subpackages/lib/source/odood/lib/devtools/package.d @@ -0,0 +1 @@ +module odood.lib.devtools; diff --git a/subpackages/lib/source/odood/lib/devtools/precommit.d b/subpackages/lib/source/odood/lib/devtools/precommit.d new file mode 100644 index 00000000..13179a06 --- /dev/null +++ b/subpackages/lib/source/odood/lib/devtools/precommit.d @@ -0,0 +1,85 @@ +module odood.lib.devtools.precommit; + +private import std.logger: warningf, infof; +private import std.exception: enforce; + +private import odood.lib.addons.repository: AddonRepository; +private import odood.exception: OdoodException; + + +// Configuration files for pre-commit for Odoo version 17 +immutable string ODOO_PRE_COMMIT_17_PRECOMMIT = import( + "pre-commit/17.0/pre-commit-config.yaml"); +immutable string ODOO_PRE_COMMIT_17_ESLINT = import( + "pre-commit/17.0/eslintrc.yml"); +immutable string ODOO_PRE_COMMIT_17_FLAKE8 = import( + "pre-commit/17.0/flake8"); +immutable string ODOO_PRE_COMMIT_17_ISORT = import( + "pre-commit/17.0/isort.cfg"); +immutable string ODOO_PRE_COMMIT_17_PYLINT = import( + "pre-commit/17.0/pylintrc"); + + +/** Check if repository has precommit configuration file + * + * Params: + * repo = repository to check + * + * Returns: + * true if repository has pre-commit config file, otherwise false + **/ +bool hasPreCommitConfig(in AddonRepository repo) { + return repo.path.join(".pre-commit-config.yaml").exists; +} + + +/** Initialize pre-commit for specified repo. + * + * Params: + * repo = repository to initialize pre-commit for. + * force = if set to true, rewrite already existing pre-commit config. + * setup = if set to true, then automatically set up pre-commit according to new configuration. + **/ +void initPreCommit(in AddonRepository repo, in bool force=false, in bool setup=true) { + enforce!OdoodException( + force || !repo.hasPreCommitConfig, + "Cannot init pre-commit. Configuration already exists"); + enforce!OdoodException( + repo.project.odoo.serie == 17, + "This feature is available only for Odoo 17 at the moment!"); + infof("Initializing pre-commit for %s repo!", repo.path); + repo.path.join(".pre-commit-config.yaml").writeFile( + ODOO_PRE_COMMIT_17_PRECOMMIT); + repo.path.join(".eslintrc.yml").writeFile( + ODOO_PRE_COMMIT_17_ESLINT); + repo.path.join(".flake8").writeFile( + ODOO_PRE_COMMIT_17_FLAKE8); + repo.path.join(".isort.cfg").writeFile( + ODOO_PRE_COMMIT_17_ISORT); + repo.path.join(".pylintrc").writeFile( + ODOO_PRE_COMMIT_17_PYLINT); + + if (setup) + setUpPreCommit(repo); +} + + +/** Set up pre-commit for specified repository. + * This means, installing pre-commit in virtualenv of related project, + * and running "pre-commit install" command. + * + * Params: + * repo = repository to initialize pre-commit for. + **/ +void setUpPreCommit(in AddonRepository repo) { + if (repo.hasPreCommitConfig) { + infof("Setting up pre-commit for %s repo!", repo.path); + repo.project.venv.installPyPackages("pre-commit"); + repo.project.venv.runE(["pre-commit", "install"]); + } else { + warningf( + "Cannot set up pre-commit for repository %s, " ~ + "because it does not have pre-commit configuration!", + repo.path); + } +} diff --git a/subpackages/lib/source/odood/lib/install/odoo.d b/subpackages/lib/source/odood/lib/install/odoo.d index f09f55c8..f107b60c 100644 --- a/subpackages/lib/source/odood/lib/install/odoo.d +++ b/subpackages/lib/source/odood/lib/install/odoo.d @@ -15,7 +15,7 @@ private import odood.exception: OdoodException; private import odood.lib.project: Project; private import odood.utils.odoo.serie: OdooSerie; -private import odood.utils.git; +private import odood.git; private import odood.utils; private import odood.utils.versioned: Version; @@ -146,6 +146,15 @@ void installOdoo(in Project project) { "xlwt", ); } else { + // Patch requirements txt to avoid using gevent 21.8.0 that requires build. + // See https://github.com/odoo/odoo/issues/187021 + info("Patching Odoo requirements.txt to avoid usage of gevent 21.8.0..."); + auto requirements_content = project.odoo.path.join("requirements.txt").readFileText() + .replaceAll( + regex(r"gevent==21\.8\.0", "g"), + "gevent==21.12.0"); + project.odoo.path.join("requirements.txt").writeFile(requirements_content); + info("Installing odoo dependencies (requirements.txt)"); project.venv.installPyRequirements( project.odoo.path.join("requirements.txt")); diff --git a/subpackages/lib/source/odood/lib/install/python.d b/subpackages/lib/source/odood/lib/install/python.d index 9b537d6a..970532de 100644 --- a/subpackages/lib/source/odood/lib/install/python.d +++ b/subpackages/lib/source/odood/lib/install/python.d @@ -7,10 +7,9 @@ private import thepath: Path; private import theprocess: resolveProgram; private import odood.lib.project: Project; -private import odood.lib.venv: PySerie; private import odood.lib.odoo.python; private import odood.utils.odoo.serie: OdooSerie; -private import odood.utils: download, parsePythonVersion; +private import odood.utils: parsePythonVersion; private import odood.utils.versioned: Version; private import odood.exception: OdoodException; @@ -49,6 +48,8 @@ bool isSystemPythonSuitable(in Project project) { return (sys_py_ver >= Version(3, 7) && sys_py_ver < Version(3, 11)); if (project.odoo.serie <= OdooSerie(17)) return (sys_py_ver >= Version(3, 10) && sys_py_ver < Version(3, 12)); + if (project.odoo.serie <= OdooSerie(18)) + return (sys_py_ver >= Version(3, 10) && sys_py_ver < Version(3, 12)); /// Unknown odoo version return false; diff --git a/subpackages/lib/source/odood/lib/odoo/lodoo.d b/subpackages/lib/source/odood/lib/odoo/lodoo.d index 868f74f6..74bb19d0 100644 --- a/subpackages/lib/source/odood/lib/odoo/lodoo.d +++ b/subpackages/lib/source/odood/lib/odoo/lodoo.d @@ -16,6 +16,8 @@ private import odood.utils: generateRandomString; private import odood.utils.odoo.db: BackupFormat; private import odood.exception: OdoodException; +// TODO: Do we need all this for Odoo 17+? It seems that it has built-in commands +// for database management /** Wrapper struct around [LOdoo](https://pypi.org/project/lodoo/) * python CLI util diff --git a/subpackages/lib/source/odood/lib/odoo/log.d b/subpackages/lib/source/odood/lib/odoo/log.d index 69f24e00..a26495b1 100644 --- a/subpackages/lib/source/odood/lib/odoo/log.d +++ b/subpackages/lib/source/odood/lib/odoo/log.d @@ -60,8 +60,7 @@ immutable auto RE_LOG_RECORD_DATA = ctRegex!( /// String representation of this message const(string) toString() const { - import std.format: format; - auto msg_truncated = msg.length > 200 ? + immutable auto msg_truncated = msg.length > 200 ? (msg[0..200] ~ "...") : msg[0..$]; return "%s %s %s %s %s %s".format( date, process_id, log_level, db, logger, msg_truncated); diff --git a/subpackages/lib/source/odood/lib/odoo/python.d b/subpackages/lib/source/odood/lib/odoo/python.d index af936616..7acb2566 100644 --- a/subpackages/lib/source/odood/lib/odoo/python.d +++ b/subpackages/lib/source/odood/lib/odoo/python.d @@ -33,14 +33,16 @@ string suggestPythonVersion(in Project project) { if (project.odoo.serie == OdooSerie(12)) return "3.7.17"; if (project.odoo.serie == OdooSerie(13)) - return "3.8.17"; + return "3.8.20"; if (project.odoo.serie == OdooSerie(14)) - return "3.8.17"; + return "3.8.20"; if (project.odoo.serie == OdooSerie(15)) - return "3.8.17"; + return "3.8.20"; if (project.odoo.serie == OdooSerie(16)) - return "3.8.17"; + return "3.8.20"; if (project.odoo.serie == OdooSerie(17)) - return "3.10.13"; - return "3.8.17"; + return "3.10.16"; + if (project.odoo.serie == OdooSerie(18)) + return "3.10.16"; + return "3.8.20"; } diff --git a/subpackages/lib/source/odood/lib/odoo/test.d b/subpackages/lib/source/odood/lib/odoo/test.d index 47e61862..e3b2fee8 100644 --- a/subpackages/lib/source/odood/lib/odoo/test.d +++ b/subpackages/lib/source/odood/lib/odoo/test.d @@ -37,8 +37,11 @@ private immutable ODOO_TEST_LONGPOLLING_PORT=8272; * Params: * project = project to generate name of test database for. **/ -string generateTestDbName(in Project project) pure { - return "odood%s-odood-test".format(project.odoo.serie.major); +string generateTestDbName(in Project project) { + string prefix = project.getOdooConfig["options"].getKey( + "db_user", + "odood%s".format(project.odoo.serie.major)); + return "%s-odood-test".format(prefix); } @@ -520,7 +523,9 @@ struct OdooTestRunner { "--log-level=warn", "--stop-after-init", "--workers=0", - "--longpolling-port=%s".format(ODOO_TEST_LONGPOLLING_PORT), + _project.odoo.serie < OdooSerie(16) ? + "--longpolling-port=%s".format(ODOO_TEST_LONGPOLLING_PORT) : + "--gevent-port=%s".format(ODOO_TEST_LONGPOLLING_PORT), opt_http_port, "--database=%s".format(_test_db_name), ] diff --git a/subpackages/lib/source/odood/lib/odoo/utils.d b/subpackages/lib/source/odood/lib/odoo/utils.d index 4590f80a..4393e179 100644 --- a/subpackages/lib/source/odood/lib/odoo/utils.d +++ b/subpackages/lib/source/odood/lib/odoo/utils.d @@ -21,6 +21,9 @@ private auto immutable RE_VERSION_CONFLICT = ctRegex!( `(?P\s+["']version["']:\s)(?P["'])(?P\d+\.\d+\.\d+\.\d+\.\d+)["'],\n` ~ `>>>>>>> .*\n`, "m"); +private auto immutable RE_MANIFEST_SERIE_VERSION = ctRegex!( + `^(?P\s+["']version["']:\s["'])(?P\d+\.\d+\.\d+\.\d+\.\d+)(?P["'],\s*(#.*)?)$`, "m"); + /// Resolve version conflict in provided manifest content. string fixVersionConflictImpl(in string manifest_content, in OdooSerie serie) { @@ -78,3 +81,47 @@ void fixVersionConflict(in Path manifest_path, in OdooSerie serie) { .fixVersionConflictImpl(serie); manifest_path.writeFile(manifest_content); } + +/// Update Odoo serie in manifest to specified. +string updateManifestSerieImpl(in string manifest_content, in OdooSerie serie) { + return manifest_content.replaceAll!((Captures!(string) captures) { + const OdooAddonVersion new_version = OdooAddonVersion(captures["addonversion"]) + .ensureIsStandard.withSerie(serie); + + // TODO: find better way. Check if head and change versions are valid + assert(new_version.isStandard, "New version is not valid!"); + + return "%s%s%s".format( + captures["verprefix"], + new_version.toString, + captures["versuffix"], + ); + })(RE_MANIFEST_SERIE_VERSION); +} + +unittest { + import unit_threaded.assertions; + string manifest_content = `{ + 'name': "Bureaucrat Helpdesk Pro [Obsolete]", + 'author': "Center of Research and Development", + 'website': "https://crnd.pro", + 'version': '16.0.1.10.0', + 'category': 'Helpdesk', +}`; + + manifest_content.updateManifestSerieImpl(OdooSerie(17)).shouldEqual(`{ + 'name': "Bureaucrat Helpdesk Pro [Obsolete]", + 'author': "Center of Research and Development", + 'website': "https://crnd.pro", + 'version': '17.0.1.10.0', + 'category': 'Helpdesk', +}`); +} + +/// Update serie in manifest +void updateManifestSerie(in Path manifest_path, in OdooSerie serie) { + infof("Updating serie in manifest: %s", manifest_path); + string manifest_content = manifest_path.readFileText() + .updateManifestSerieImpl(serie); + manifest_path.writeFile(manifest_content); +} diff --git a/subpackages/lib/source/odood/lib/package.d b/subpackages/lib/source/odood/lib/package.d index eef84386..beaa5535 100644 --- a/subpackages/lib/source/odood/lib/package.d +++ b/subpackages/lib/source/odood/lib/package.d @@ -1,6 +1,6 @@ module odood.lib; -public immutable string _version = "0.1.0"; +public immutable string _version = "0.2.0"; public import odood.lib.project; diff --git a/subpackages/lib/source/odood/lib/project/config.d b/subpackages/lib/source/odood/lib/project/config.d index 7df5bfc2..7797a98c 100644 --- a/subpackages/lib/source/odood/lib/project/config.d +++ b/subpackages/lib/source/odood/lib/project/config.d @@ -19,6 +19,9 @@ enum ProjectServerSupervisor { /// Server is managed by init script in /etc/init.d odoo InitScript, + + /// Server is managed by systemd + Systemd, } @@ -59,9 +62,12 @@ struct ProjectConfigOdoo { /// Managed by OS. ProjectServerSupervisor server_supervisor = ProjectServerSupervisor.Odood; - /// Path to init script, of project's server is managed by init script. + /// Path to init script, if project's server is managed by init script. Path server_init_script_path; + /// Path to systemd service configuration, if project's server is managed by systemd. + Path server_systemd_service_path; + this(in Path project_root, in ProjectConfigDirectories directories, in OdooSerie odoo_serie, @@ -80,6 +86,17 @@ struct ProjectConfigOdoo { repo = odoo_repo.empty ? DEFAULT_ODOO_REPO : odoo_repo; } + this(in Path project_root, + in ProjectConfigDirectories directories, + in OdooSerie odoo_serie) { + this( + project_root, + directories, + odoo_serie, + odoo_serie.toString, // Default branch for serie + DEFAULT_ODOO_REPO); + } + this(in dyaml.Node config) { /* TODO: think about following structure of test config in yml: * odoo: @@ -114,6 +131,9 @@ struct ProjectConfigOdoo { case "init-script": this.server_supervisor = ProjectServerSupervisor.InitScript; break; + case "systemd": + this.server_supervisor = ProjectServerSupervisor.Systemd; + break; default: assert( 0, @@ -122,6 +142,10 @@ struct ProjectConfigOdoo { } else this.server_supervisor = ProjectServerSupervisor.Odood; + if (config.containsKey("server-init-script-path")) + this.server_init_script_path = Path(config["server-init-script-path"].as!string); + if (config.containsKey("server-systemd-service-path")) + this.server_systemd_service_path = Path(config["server-systemd-service-path"].as!string); } dyaml.Node toYAML() const { @@ -145,6 +169,11 @@ struct ProjectConfigOdoo { break; case ProjectServerSupervisor.InitScript: result["server-supervisor"] = "init-script"; + result["server-init-script-path"] = this.server_init_script_path.toString; + break; + case ProjectServerSupervisor.Systemd: + result["server-supervisor"] = "systemd"; + result["server-systemd-service-path"] = this.server_systemd_service_path.toString; break; } diff --git a/subpackages/lib/source/odood/lib/project/project.d b/subpackages/lib/source/odood/lib/project/project.d index 6639fec3..6e09ae4c 100644 --- a/subpackages/lib/source/odood/lib/project/project.d +++ b/subpackages/lib/source/odood/lib/project/project.d @@ -27,7 +27,7 @@ public import odood.lib.project.config: ProjectConfigOdoo, ProjectConfigDirectories, DEFAULT_ODOO_REPO; private import odood.utils.odoo.serie: OdooSerie; -private import odood.utils.git: isGitRepo; +private import odood.git: isGitRepo, GitRepository; private import odood.utils: generateRandomString; @@ -38,6 +38,9 @@ enum OdooInstallType { Git, } +/** Define path for odood system-wide project config + **/ +immutable auto ODOOD_SYSTEM_CONFIG_PATH = Path("/", "etc", "odood.yml"); /** The Odood project. * The main entity to manage whole Odood project @@ -63,8 +66,9 @@ class Project { // If config is not found in current directory and above, // check server-wide config (may be it is installed in server-mode) - if (s_config_path.isNull && Path("/", "etc", "odood.yml").exists) - s_config_path = Path("/", "etc", "odood.yml").nullable; + if (s_config_path.isNull && ODOOD_SYSTEM_CONFIG_PATH.exists) + // We have to copy Path, because nullable does not work on immutable path. + s_config_path = Path(ODOOD_SYSTEM_CONFIG_PATH.toString).nullable; enforce!OdoodException( !s_config_path.isNull, @@ -420,19 +424,12 @@ class Project { auto tag_name = "%s-before-update-%s".format( this.odoo.serie, dt_string); - Process("git") - .withArgs( - "tag", - "-a", tag_name, - "-m", "Save before odoo update (%s)".format(dt_string)) - .inWorkDir(this.odoo.path) - .execute() - .ensureOk(true); - Process("git") - .withArgs("pull") - .inWorkDir(this.odoo.path) - .execute() - .ensureOk(true); + auto repo = new GitRepository(this.odoo.path); + repo.setTag( + tag_name, + "Save before odoo update (%s)".format(dt_string)); + repo.pull(); + break; } this.installOdoo(); diff --git a/subpackages/lib/source/odood/lib/server/exception.d b/subpackages/lib/source/odood/lib/server/exception.d index 7ba5f0c1..4089dbaf 100644 --- a/subpackages/lib/source/odood/lib/server/exception.d +++ b/subpackages/lib/source/odood/lib/server/exception.d @@ -14,10 +14,3 @@ class ServerAlreadyRuningException : ServerException { mixin basicExceptionCtors; } - - -class ServerCommandFailedException : ServerException -{ - mixin basicExceptionCtors; -} - diff --git a/subpackages/lib/source/odood/lib/server/server.d b/subpackages/lib/source/odood/lib/server/server.d index b9e92dd2..961b45ec 100644 --- a/subpackages/lib/source/odood/lib/server/server.d +++ b/subpackages/lib/source/odood/lib/server/server.d @@ -83,13 +83,22 @@ struct OdooServer { * - -2 if process specified in pid file is not running **/ pid_t getPid() const { - if (_project.odoo.pidfile.exists) { - auto pid = _project.odoo.pidfile.readFileText.strip.to!pid_t; - if (isProcessRunning(pid)) - return pid; - return -2; + final switch(_project.odoo.server_supervisor) { + case ProjectServerSupervisor.Odood, ProjectServerSupervisor.InitScript: + if (_project.odoo.pidfile.exists) { + auto pid = _project.odoo.pidfile.readFileText.strip.to!pid_t; + if (isProcessRunning(pid)) + return pid; + return -2; + } + return -1; + case ProjectServerSupervisor.Systemd: + return Process("systemctl") + .withArgs("show", "--property=MainPID", "--value", "odoo") + .execute + .ensureOk(true) + .output.to!pid_t; } - return -1; } /** Get environment variables to apply when running Odoo server. @@ -109,6 +118,8 @@ struct OdooServer { res["OPENERP_SERVER"] = _project.odoo.configfile.toString; res["ODOO_RC"] = _project.odoo.configfile.toString; } + // TODO: Add ability to parse .env files and forward environment variables to Odoo process + // This will allow to run Odoo in docker containers and locally in similar way. return res; } @@ -167,8 +178,7 @@ struct OdooServer { * detach = if set, then run server in background **/ pid_t spawn(bool detach=false) const { - import std.process: Config; - + // TODO: Add ability to handle coverage settings and other odoo options enforce!ServerAlreadyRuningException( !isRunning, "Server already running!"); @@ -180,11 +190,13 @@ struct OdooServer { auto runner = getServerRunner( "--pidfile=%s".format(_project.odoo.pidfile)); if (detach) { - runner.setFlag(Config.detached); + runner.setFlag(std.process.Config.detached); runner.addArgs("--logfile=%s".format(_project.odoo.logfile)); } if (_project.odoo.pidfile.exists) { + // At this point it is already checked that server is not running, + // thus it is safe to delete stale pid file. tracef("Removing pidfile %s before server starts...", _project.odoo.pidfile); _project.odoo.pidfile.remove(); } @@ -220,45 +232,6 @@ struct OdooServer { return pipeServerLog(CoverageOptions(false), options); } - /** Run server with provided options. - * - * Params: - * options = list of options to pass to the server - * env = extra environment variables to pass to the server - **/ - auto run(in string[] options, in string[string] env=null) const { - auto res = _project.venv.run( - scriptPath, - options, - _project.project_root, - getServerEnv(env)); - - return res; - } - - /// ditto - auto run(in string[] options...) const { - return run(options, null); - } - - /** Run server with provided options - * - * In case of non-zero exit code error will be raised. - * - * Params: - * options = list of options to pass to the server - * env = extra environment variables to pass to the server - **/ - auto runE(in string[] options, in string[string] env=null) const { - auto result = run(options, env).ensureStatus!ServerCommandFailedException(true); - return result; - } - - /// ditto - auto runE(in string[] options...) const { - return runE(options, null); - } - /** Check if the Odoo server is running or not * **/ @@ -288,7 +261,13 @@ struct OdooServer { Process("/etc/init.d/odoo") .withArgs("start") .execute - .ensureOk(); + .ensureOk(true); + break; + case ProjectServerSupervisor.Systemd: + Process("service") + .withArgs("odoo", "start") + .execute + .ensureOk(true); break; } if (wait_timeout != Duration.zero) { @@ -307,7 +286,7 @@ struct OdooServer { **/ void stopOdoodServer() const { import core.sys.posix.signal: kill, SIGTERM; - import core.stdc.errno; + import core.stdc.errno: errno, ESRCH; import std.exception: ErrnoException; info("Stopping odoo server..."); @@ -350,7 +329,13 @@ struct OdooServer { Process("/etc/init.d/odoo") .withArgs("stop") .execute - .ensureOk(); + .ensureOk(true); + break; + case ProjectServerSupervisor.Systemd: + Process("service") + .withArgs("odoo", "stop") + .execute + .ensureOk(true); break; } } diff --git a/subpackages/lib/source/odood/lib/venv.d b/subpackages/lib/source/odood/lib/venv.d index 77c8282e..aab722fc 100644 --- a/subpackages/lib/source/odood/lib/venv.d +++ b/subpackages/lib/source/odood/lib/venv.d @@ -6,6 +6,7 @@ private import std.typecons: Nullable; private import std.exception: enforce; private import std.conv: to; private import std.parallelism: totalCPUs; +private import std.regex: ctRegex, matchFirst; private static import std.process; @@ -68,6 +69,9 @@ const struct VirtualEnv { /// Path where virtualenv isntalled @safe pure nothrow const(Path) path() const { return _path; } + /// Bin path inside this virtualenv + @safe pure nothrow const(Path) bin_path() const {return _path.join("bin"); } + /// Serie of python used for this virtualenv (py2 or py3) @safe const(PySerie) py_serie() const { return _py_serie; } @@ -247,7 +251,6 @@ const struct VirtualEnv { /// ditto void buildPython(in Version build_version, in bool enable_sqlite=false) { - import std.regex: ctRegex, matchFirst; infof("Building python version %s...", build_version); diff --git a/subpackages/utils/dub.sdl b/subpackages/utils/dub.sdl index e8fa9b64..15caa820 100644 --- a/subpackages/utils/dub.sdl +++ b/subpackages/utils/dub.sdl @@ -6,7 +6,7 @@ license "MPL-2.0" dependency "requests" version=">=2.0.0" dependency "thepath" version=">=1.2.0" -dependency "theprocess" version=">=0.0.5" +dependency "theprocess" version=">=0.0.7" dependency "zipper" version="~>0.0.5" targetPath "build" diff --git a/subpackages/utils/dub.selections.json b/subpackages/utils/dub.selections.json index 7a872dd3..f8ec19fc 100644 --- a/subpackages/utils/dub.selections.json +++ b/subpackages/utils/dub.selections.json @@ -8,8 +8,8 @@ "odood": {"path":"../../"}, "prettyprint": "1.0.9", "requests": "2.1.3", - "thepath": "1.2.0", - "theprocess": "0.0.5", + "thepath": "2.0.0", + "theprocess": "0.0.7", "unit-threaded": "2.2.0", "zipper": "0.0.5" } diff --git a/subpackages/utils/source/odood/utils/versioned.d b/subpackages/utils/source/odood/utils/versioned.d index 66d42854..e02e1eeb 100644 --- a/subpackages/utils/source/odood/utils/versioned.d +++ b/subpackages/utils/source/odood/utils/versioned.d @@ -1,6 +1,6 @@ module odood.utils.versioned; -private import std.conv: to, ConvOverflowException; +private import std.conv: to, ConvOverflowException, ConvException; private import std.range: empty, zip; private import std.algorithm.searching: canFind; private import std.string : isNumeric; @@ -16,7 +16,7 @@ private enum VersionPart { } -@safe struct Version { +@safe pure struct Version { private uint _major=0; private uint _minor=0; private uint _patch=0; @@ -24,7 +24,7 @@ private enum VersionPart { private string _build; private bool _isValid; - this(in uint major, in uint minor=0, in uint patch=0) { + this(in uint major, in uint minor=0, in uint patch=0) nothrow { _major = major; _minor = minor; _patch = patch; @@ -32,6 +32,17 @@ private enum VersionPart { } this(in string v) { + try { + parseVersionString(v); + _isValid = true; + } catch (ConvException) { + // Cannot convert one of version parts to uint, + // thus version is not valid + _isValid = false; + } + } + + private void parseVersionString(in string v) { // TODO: Add validation // TODO: Add support of 'v' prefix if (v.length == 0) return; @@ -127,19 +138,18 @@ private enum VersionPart { break; } } - _isValid = true; } - pure nothrow uint major() const { return _major; } - pure nothrow uint minor() const { return _minor; } - pure nothrow uint patch() const { return _patch; } - pure nothrow string prerelease() const { return _prerelease; } - pure nothrow string build() const { return _build; } + nothrow uint major() const { return _major; } + nothrow uint minor() const { return _minor; } + nothrow uint patch() const { return _patch; } + nothrow string prerelease() const { return _prerelease; } + nothrow string build() const { return _build; } - pure nothrow bool isValid() const { return _isValid; } - pure nothrow bool isStable() const { return _prerelease.empty; } + nothrow bool isValid() const { return _isValid; } + nothrow bool isStable() const { return _prerelease.empty; } - pure nothrow string toString() const { + nothrow string toString() const { string result = _major.to!string ~ "." ~ _minor.to!string ~ "." ~ _patch.to!string; if (_prerelease.length > 0) @@ -149,6 +159,7 @@ private enum VersionPart { return result; } + /// unittest { import unit_threaded.assertions; Version("1.2.3").major.should == 1; @@ -233,7 +244,16 @@ private enum VersionPart { Version("12.34.56+build-42").isValid.should == true; } - pure int opCmp(in Version other) const { + // Test invalid versions + unittest { + import unit_threaded.assertions; + Version("2s").isValid.should == false; + Version("2.3s").isValid.should == false; + Version("2.3.4s").isValid.should == false; + Version("2.3.4-s").isValid.should == true; + } + + int opCmp(in Version other) const { // TODO: make it nothrow if (this.major != other.major) return this.major < other.major ? -1 : 1; @@ -243,7 +263,7 @@ private enum VersionPart { return this.patch < other.patch ? -1 : 1; // Just copypaste from semver lib - int compareSufix(scope const string[] suffix, const string[] anotherSuffix) @safe pure + int compareSufix(scope const string[] suffix, const string[] anotherSuffix) { if (!suffix.empty && anotherSuffix.empty) return -1; @@ -282,6 +302,12 @@ private enum VersionPart { return result; } + /// ditto + int opCmp(in string other) const { + return this.opCmp(Version(other)); + } + + /// Test version comparisons unittest { import unit_threaded.assertions; assert(Version("1.0.0-alpha") < Version("1.0.0-alpha.1")); @@ -297,7 +323,23 @@ private enum VersionPart { assert(Version("1.0.0-rc.2+build.5") != Version("1.0.0-rc.1+build.5")); } - bool opEquals(in Version other) const pure nothrow { + /// Test comparisons with strings + unittest { + import unit_threaded.assertions; + assert(Version("1.0.0-alpha") < "1.0.0-alpha.1"); + assert(Version("1.0.0-alpha.1") < "1.0.0-alpha.beta"); + assert(Version("1.0.0-alpha.beta") < "1.0.0-beta"); + assert(Version("1.0.0-beta") < "1.0.0-beta.2"); + assert(Version("1.0.0-beta.2") < "1.0.0-beta.11"); + assert(Version("1.0.0-beta.11") < "1.0.0-rc.1"); + assert(Version("1.0.0-rc.1") < "1.0.0"); + assert(Version("1.0.0-rc.1") > "1.0.0-rc.1+build.5"); + assert(Version("1.0.0-rc.1+build.5") == "1.0.0-rc.1+build.5"); + assert(Version("1.0.0-rc.1+build.5") != "1.0.0-rc.1+build.6"); + assert(Version("1.0.0-rc.2+build.5") != "1.0.0-rc.1+build.5"); + } + + bool opEquals(in Version other) const nothrow { return this.major == other.major && this.minor == other.minor && this.patch == other.patch && @@ -305,6 +347,12 @@ private enum VersionPart { this.build == other.build; } + /// ditto + bool opEquals(in string other) const { + return this.opEquals(Version(other)); + } + + /// Test equality checks unittest { import unit_threaded.assertions; Version("1.2.3").should == Version(1, 2, 3); @@ -313,4 +361,12 @@ private enum VersionPart { // TODO: more tests needed } + /// Test equality checks with strings + unittest { + import unit_threaded.assertions; + assert(Version("1.2.3") == "1.2.3"); + assert(Version("1.2") == "1.2"); + assert(Version("1.0.3") == "1.0.3"); + // TODO: more tests needed + } } diff --git a/tests/basic.d b/tests/basic.d index 7c146942..758c9c05 100644 --- a/tests/basic.d +++ b/tests/basic.d @@ -244,6 +244,55 @@ void runBasicTests(in Project project) { // TODO: Complete the test } +@("Basic Test Odoo 18") +unittest { + auto temp_path = createTempPath( + environment.get("TEST_ODOO_TEMP", std.file.tempDir), + "tmp-odood-18", + ); + scope(exit) temp_path.remove(); + + // Create database use for odoo 17 instance + createDbUser("odood18test", "odoo"); + + auto project = new Project(temp_path, OdooSerie(18)); + auto odoo_conf = OdooConfigBuilder(project) + .setDBConfig( + environment.get("POSTGRES_HOST", "localhost"), + environment.get("POSTGRES_PORT", "5432"), + "odood18test", + "odoo") + .setHttp("localhost", "18069") + .result(); + project.initialize(odoo_conf); + project.save(); + + // Test created project + project.project_root.shouldEqual(temp_path); + project.odoo.serie.shouldEqual(OdooSerie(18)); + project.config_path.shouldEqual(temp_path.join("odood.yml")); + + // Run basic tests + //project.runBasicTests; + + /* + * TODO: Currently, because some addons used in tests are not ported to 17, + * we do not test addons management. But later, when that addons ported + * we have to chage this and run tests for addons management for Odoo 17 + */ + + // Test server management + testServerManagement(project); + + // Test LOdoo Database operations + testDatabaseManagement(project); + + // Test basic addons management + //testAddonsManagementBasic(project); + + // Test running scripts + testRunningScripts(project); +} @("Basic Test Odoo 17") unittest { @@ -494,7 +543,7 @@ unittest { testServerManagement(project); // Test that server initialization works fine - project.server.run("--stop-after-init", "--no-http"); + project.server.getServerRunner("--stop-after-init", "--no-http").execute; // Reinstall Odoo to version 15 project.reinstallOdoo(OdooSerie(15), true); @@ -505,7 +554,7 @@ unittest { project.config_path.shouldEqual(temp_path.join("odood.yml")); // Test that server initialization works fine - project.server.run("--stop-after-init", "--no-http"); + project.server.getServerRunner("--stop-after-init", "--no-http").execute; // Run basic tests project.runBasicTests;