diff --git a/.env.example b/.env.example index 5787e6288..e54f922d7 100644 --- a/.env.example +++ b/.env.example @@ -88,7 +88,9 @@ EMAIL_HOST_USER=user EMAIL_HOST_PASSWORD=password DEFAULT_FROM_EMAIL="webmaster@localhost" -QFIELDCLOUD_DEFAULT_NETWORK=qfieldcloud_default +# Docker compose default network also used by the docker in docker workers +# If empty value, a default name will be generated at build time, for example `qfieldcloud_default`. +# QFIELDCLOUD_DEFAULT_NETWORK="" # Admin URI. Requires slash in the end. Please use something that is hard to guess. QFIELDCLOUD_ADMIN_URI=admin/ @@ -122,6 +124,11 @@ QFIELDCLOUD_DEFAULT_TIME_ZONE="Europe/Zurich" # DEFAULT: "" QFIELDCLOUD_LIBQFIELDSYNC_VOLUME_PATH="" +# QFieldCloud SDK volume path to be mounted by the `worker_wrapper` into `worker` containers. +# If empty value or invalid value, the pip installed version defined in `requirements_libqfieldsync.txt` will be used. +# DEFAULT: "" +QFIELDCLOUD_QFIELDCLOUD_SDK_VOLUME_PATH="" + # The Django development port. Not used in production. # DEFAULT: 8011 DJANGO_DEV_PORT=8011 @@ -162,7 +169,3 @@ DEBUG_DEBUGPY_APP_PORT=5678 # Debugpy port used for the `worker_wrapper` service # DEFAULT: 5679 DEBUG_DEBUGPY_WORKER_WRAPPER_PORT=5679 - -# Path to the nginx, letsencrypt, etc configuration files, used by script in `./scripts/`. -# DEFAULT: ./conf -CONFIG_PATH=./conf diff --git a/.gitignore b/.gitignore index 23825ee50..23d81b37d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ conf/certbot/* Pipfile* **/site-packages docker-qgis/libqfieldsync +docker-qgis/qfieldcloud-sdk-python diff --git a/docker-app/qfieldcloud/core/admin.py b/docker-app/qfieldcloud/core/admin.py index 91636c340..74874c3e8 100644 --- a/docker-app/qfieldcloud/core/admin.py +++ b/docker-app/qfieldcloud/core/admin.py @@ -19,7 +19,7 @@ from django.contrib import admin, messages from django.contrib.admin.templatetags.admin_urls import admin_urlname from django.contrib.admin.views.main import ChangeList -from django.core.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied, ValidationError from django.db.models import Q, QuerySet from django.db.models.fields.json import JSONField from django.db.models.functions import Lower @@ -31,6 +31,7 @@ from django.urls import path, reverse from django.utils.decorators import method_decorator from django.utils.html import escape, format_html +from django.utils.http import urlencode from django.utils.safestring import SafeText from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache @@ -48,6 +49,7 @@ Person, Project, ProjectCollaborator, + Secret, Team, TeamMember, User, @@ -55,7 +57,7 @@ ) from qfieldcloud.core.paginators import LargeTablePaginator from qfieldcloud.core.templatetags.filters import filesizeformat10 -from qfieldcloud.core.utils2 import delta_utils, jobs +from qfieldcloud.core.utils2 import delta_utils, jobs, pg_service_file from rest_framework.authtoken.models import TokenProxy admin.site.unregister(LogEntry) @@ -115,6 +117,15 @@ def has_delete_permission(self, request, obj=None): return super().has_delete_permission(request, obj) +class QFieldCloudInlineAdmin(admin.TabularInline): + template = "admin/edit_inline/tabular_customized.html" + + def get_formset(self, request, obj=None, **kwargs): + self.parent_obj = obj + + return super().get_formset(request, obj, **kwargs) + + def admin_urlname_by_obj(value, arg): if isinstance(value, User): if value.is_person: @@ -584,6 +595,132 @@ def queryset(self, request, queryset): return queryset.filter(owner__type=value) +class ProjectSecretForm(ModelForm): + class Meta: + model = Secret + fields = ("project", "name", "type", "value", "created_by") + + name = fields.CharField(widget=widgets.TextInput) + value = fields.CharField(widget=widgets.Textarea) + + def get_initial_for_field(self, field, field_name): + if self.instance.pk and field_name == "value": + return "" + return super().get_initial_for_field(field, field_name) + + def clean(self): + cleaned_data = super().clean() + + if self.instance.pk: + type = self.instance.type + else: + type = cleaned_data.get("type") + if type == Secret.Type.PGSERVICE: + # validate the pg_service.conf + value = cleaned_data.get("value") + if value: + try: + pg_service_file.validate_pg_service_conf(value) + except ValidationError as err: + raise ValidationError({"value": err.message}) + + # ensure name with PGSERVICE_SECRET_NAME_PREFIX + name = cleaned_data.get("name") + if name and not name.startswith( + pg_service_file.PGSERVICE_SECRET_NAME_PREFIX + ): + cleaned_data[ + "name" + ] = f"{pg_service_file.PGSERVICE_SECRET_NAME_PREFIX}{name}" + + return cleaned_data + + +class SecretAdmin(QFieldCloudModelAdmin): + model = Secret + form = ProjectSecretForm + fields = ("project", "name", "type", "value", "created_by") + readonly_fields = ("created_by",) + list_display = ("name", "type", "created_by__link", "project__name") + autocomplete_fields = ("project",) + + search_fields = ( + "name__icontains", + "project__name__icontains", + ) + + @admin.display(ordering="created_by", description=_("Created by")) + def created_by__link(self, instance): + return model_admin_url(instance.created_by) + + @admin.display(ordering="project__name") + def project__name(self, instance): + return model_admin_url(instance.project, instance.project.name) + + def get_readonly_fields(self, request, obj=None): + readonly_fields = super().get_readonly_fields(request, obj) + + if obj: + return (*readonly_fields, "name", "type", "project") + + return readonly_fields + + def save_model(self, request, obj, form, change): + # only set created_by during the first save + if not change: + obj.created_by = request.user + super().save_model(request, obj, form, change) + + def get_changeform_initial_data(self, request): + project_id = request.GET.get("project_id") + + if project_id: + project = Project.objects.get(id=project_id) + else: + project = None + + return {"project": project} + + +class ProjectSecretInline(QFieldCloudInlineAdmin): + model = Secret + fields = ("link_to_secret", "type", "created_by") + readonly_fields = ("link_to_secret",) + max_num = 0 + extra = 0 + + @admin.display(description=_("Name")) + def link_to_secret(self, obj): + url = reverse("admin:core_secret_change", args=[obj.pk]) + return format_html('{}', url, obj.name) + + def has_add_permission(self, request, obj=None): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + @property + def bottom_html(self): + if self.parent_obj: + return format_html( + """ + + + {text} + + """, + url=reverse("admin:core_secret_add"), + query_params=urlencode({"project_id": self.parent_obj.pk}), + text="Add Secret", + ) + else: + return "" + + class ProjectForm(ModelForm): project_files = fields.CharField( disabled=True, required=False, widget=ProjectFilesWidget @@ -642,7 +779,7 @@ class ProjectAdmin(QFieldCloudModelAdmin): "data_last_packaged_at", "project_details__pre", ) - inlines = (ProjectCollaboratorInline,) + inlines = (ProjectCollaboratorInline, ProjectSecretInline) search_fields = ( "id", "name__icontains", @@ -652,6 +789,8 @@ class ProjectAdmin(QFieldCloudModelAdmin): ordering = ("-updated_at",) + change_form_template = "admin/project_change_form.html" + def get_form(self, *args, **kwargs): help_texts = { "file_storage_bytes": _( @@ -778,6 +917,7 @@ class JobAdmin(QFieldCloudModelAdmin): "project__name__iexact", "project__owner__username__iexact", "id", + "project__id__iexact", ) readonly_fields = ( "project", @@ -1365,6 +1505,7 @@ class LogEntryAdmin( admin.site.register(Organization, OrganizationAdmin) admin.site.register(Team, TeamAdmin) admin.site.register(Project, ProjectAdmin) +admin.site.register(Secret, SecretAdmin) admin.site.register(Delta, DeltaAdmin) admin.site.register(Job, JobAdmin) admin.site.register(Geodb, GeodbAdmin) diff --git a/docker-app/qfieldcloud/core/cron.py b/docker-app/qfieldcloud/core/cron.py index b9a72374f..9b8653763 100644 --- a/docker-app/qfieldcloud/core/cron.py +++ b/docker-app/qfieldcloud/core/cron.py @@ -7,7 +7,7 @@ from invitations.utils import get_invitation_model from sentry_sdk import capture_message -from ..core.models import Job, Project +from ..core.models import ApplyJob, ApplyJobDelta, Delta, Job, Project from ..core.utils2 import storage from .invitations_utils import send_invitation @@ -60,6 +60,22 @@ def do(self): capture_message( f'Job "{job.id}" was with status "{job.status}", but worker container no longer exists. Job unexpectedly terminated.' ) + if job.type == Job.Type.DELTA_APPLY: + ApplyJob.objects.get(id=job.id).deltas_to_apply.update( + last_status=Delta.Status.ERROR, + last_feedback=None, + last_modified_pk=None, + last_apply_attempt_at=job.started_at, + last_apply_attempt_by=job.created_by, + ) + + ApplyJobDelta.objects.filter( + apply_job_id=job.id, + ).update( + status=Delta.Status.ERROR, + feedback=None, + modified_pk=None, + ) jobs.update( status=Job.Status.FAILED, diff --git a/docker-app/qfieldcloud/core/templates/admin/edit_inline/tabular_customized.html b/docker-app/qfieldcloud/core/templates/admin/edit_inline/tabular_customized.html new file mode 100644 index 000000000..e678f8459 --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/admin/edit_inline/tabular_customized.html @@ -0,0 +1,5 @@ +{% if qfc_admin_inline_included != 1 %} + {% include "admin/edit_inline/tabular_extended.html" with qfc_admin_inline_included=1 %} +{% endif %} + +{{ fieldset.opts.bottom_html }} diff --git a/docker-app/qfieldcloud/core/templates/admin/edit_inline/tabular_extended.html b/docker-app/qfieldcloud/core/templates/admin/edit_inline/tabular_extended.html new file mode 100644 index 000000000..012e71e9e --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/admin/edit_inline/tabular_extended.html @@ -0,0 +1 @@ +{% extends "admin/edit_inline/tabular.html" %} diff --git a/docker-app/qfieldcloud/core/templates/admin/project_change_form.html b/docker-app/qfieldcloud/core/templates/admin/project_change_form.html new file mode 100644 index 000000000..cb0194217 --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/admin/project_change_form.html @@ -0,0 +1,10 @@ +{% extends 'admin/change_form.html' %} +{% load i18n %} + +{% block submit_buttons_bottom %} + {{ block.super }} + +
+ {% trans 'Project jobs' %} +
+{% endblock %} diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index a14858198..c6f7aa59d 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -4,10 +4,10 @@

 
-        
+        
     
- @@ -44,16 +44,16 @@ - + - - - + + @@ -68,27 +68,6 @@ overflow-y: auto; } - .qfc-admin-projects-files-actions-reload-btn, - .qfc-admin-projects-files-actions-info-btn, - .qfc-admin-projects-files-actions-delete-btn, - .qfc-admin-projects-files-actions-download-btn { - padding: 8px 15px; - border-radius: 4px; - cursor: pointer; - transition: background 0.15s; - vertical-align: middle; - font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif; - font-weight: normal; - font-size: 13px; - border: none; - background: var(--button-bg); - color: var(--button-fg); - } - - .qfc-admin-projects-files-actions-delete-btn { - background: var(--delete-button-bg); - } - .qfc-admin-project-files-modal { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ diff --git a/docker-app/qfieldcloud/core/tests/test_delta.py b/docker-app/qfieldcloud/core/tests/test_delta.py index c605195cc..b4ad03925 100644 --- a/docker-app/qfieldcloud/core/tests/test_delta.py +++ b/docker-app/qfieldcloud/core/tests/test_delta.py @@ -791,7 +791,7 @@ def test_non_spatial_geom_empty_str_delta(self): self.assertEqual( self.get_file_contents(project, "nonspatial.csv"), - b'fid,col1\n"1",foo\n"2",newfeature\n', + b'fid,col1\n"1",new_value\n', ) def test_special_data_types(self): diff --git a/docker-app/qfieldcloud/core/tests/testdata/delta/deltas/nonspatial_geom_empty_str.json b/docker-app/qfieldcloud/core/tests/testdata/delta/deltas/nonspatial_geom_empty_str.json index b9ee5e98d..070a21116 100644 --- a/docker-app/qfieldcloud/core/tests/testdata/delta/deltas/nonspatial_geom_empty_str.json +++ b/docker-app/qfieldcloud/core/tests/testdata/delta/deltas/nonspatial_geom_empty_str.json @@ -12,13 +12,13 @@ "new": { "geometry": "", "attributes": { - "col1": "foo" + "col1": "new_value" } }, "old": { "geometry": null, "attributes": { - "col1": "bar" + "col1": "foo" } } } diff --git a/docker-app/qfieldcloud/core/utils2/pg_service_file.py b/docker-app/qfieldcloud/core/utils2/pg_service_file.py new file mode 100644 index 000000000..609c3cf39 --- /dev/null +++ b/docker-app/qfieldcloud/core/utils2/pg_service_file.py @@ -0,0 +1,24 @@ +import configparser +import io + +from django.core.exceptions import ValidationError +from django.utils.translation import gettext as _ + +PGSERVICE_SECRET_NAME_PREFIX = "PG_SERVICE_" + + +def validate_pg_service_conf(value: str) -> None: + """Checks if a string is a valid `pg_service.conf` file contents, otherwise throws a `ValueError`""" + try: + buffer = io.StringIO(value) + config = configparser.ConfigParser() + config.readfp(buffer) + + if len(config.sections()) != 1: + raise ValidationError( + _("The `.pg_service.conf` must have exactly one service definition.") + ) + except ValidationError as err: + raise err + except Exception: + raise ValidationError(_("Failed to parse the `.pg_service.conf` file.")) diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index ca3cc0d59..76f5a12d9 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -629,7 +629,7 @@ def get_project_file_storage_in_bytes(project_id: str) -> int: total_bytes = 0 prefix = f"projects/{project_id}/files/" - logger.info(f"Project file storage size requrested for {project_id=}") + logger.info(f"Project file storage size requested for {project_id=}") if not re.match(r"^projects/[\w]{8}(-[\w]{4}){3}-[\w]{12}/files/$", prefix): raise RuntimeError( diff --git a/docker-app/qfieldcloud/core/views/files_views.py b/docker-app/qfieldcloud/core/views/files_views.py index a9fcf49c5..463f329a1 100644 --- a/docker-app/qfieldcloud/core/views/files_views.py +++ b/docker-app/qfieldcloud/core/views/files_views.py @@ -83,6 +83,8 @@ def get(self, request: Request, projectid: str) -> Response: path = PurePath(version.key) filename = str(path.relative_to(*path.parts[:3])) last_modified = version.last_modified.strftime("%d.%m.%Y %H:%M:%S %Z") + # NOTE ETag is a MD5. But for the multipart uploaded files, the MD5 is computed from the concatenation of the MD5s of each uploaded part. + # TODO make sure when file metadata is in the DB (QF-2760), this is a real md5sum of the current file. md5sum = version.e_tag.replace('"', "") version_data = { diff --git a/docker-app/qfieldcloud/settings.py b/docker-app/qfieldcloud/settings.py index bac112323..d6ac6c4aa 100644 --- a/docker-app/qfieldcloud/settings.py +++ b/docker-app/qfieldcloud/settings.py @@ -516,6 +516,11 @@ def before_send(event, hint): "QFIELDCLOUD_LIBQFIELDSYNC_VOLUME_PATH" ) +# Absolute path on the docker host where `qfieldcloud-sdk-python` is mounted from for development +QFIELDCLOUD_QFIELDCLOUD_SDK_VOLUME_PATH = os.environ.get( + "QFIELDCLOUD_QFIELDCLOUD_SDK_VOLUME_PATH" +) + # Volume name where transformation grids required by `PROJ` are downloaded to QFIELDCLOUD_TRANSFORMATION_GRIDS_VOLUME_NAME = os.environ.get( "QFIELDCLOUD_TRANSFORMATION_GRIDS_VOLUME_NAME" diff --git a/docker-app/requirements.txt b/docker-app/requirements.txt index 1b3b16c54..203da289f 100644 --- a/docker-app/requirements.txt +++ b/docker-app/requirements.txt @@ -45,7 +45,7 @@ django-tables2==2.7.0 django-timezone-field==6.1.0 djangorestframework==3.14.0 drf-spectacular==0.26.3 -idna==3.4 +idna==3.7 inflection==0.5.1 itypes==1.2.0 Jinja2==3.1.3 @@ -68,7 +68,7 @@ pyrsistent==0.19.3 python-dateutil==2.8.2 python3-openid==3.2.0 pytz==2023.3 -requests==2.31.0 +requests==2.32.2 requests-oauthlib==1.3.1 ruamel.yaml==0.17.26 ruamel.yaml.clib==0.2.7 @@ -76,7 +76,7 @@ s3transfer==0.5.0 sentry-sdk==1.24.0 six==1.16.0 soupsieve==2.4.1 -sqlparse==0.4.4 +sqlparse==0.5.0 stripe==4.2.0 swapper==1.3.0 typing-extensions==4.6.0 diff --git a/docker-app/requirements_runtime.txt b/docker-app/requirements_runtime.txt index 9d41f264a..5389b4df8 100644 --- a/docker-app/requirements_runtime.txt +++ b/docker-app/requirements_runtime.txt @@ -1 +1 @@ -gunicorn==20.1.0 +gunicorn==22.0.0 diff --git a/docker-app/requirements_worker_wrapper.txt b/docker-app/requirements_worker_wrapper.txt index d224175c7..ae0988ff3 100644 --- a/docker-app/requirements_worker_wrapper.txt +++ b/docker-app/requirements_worker_wrapper.txt @@ -1,2 +1,2 @@ -docker==4.2.2 -tenacity==8.1.0 +docker==7.1.0 +tenacity==8.3.0 diff --git a/docker-app/worker_wrapper/wrapper.py b/docker-app/worker_wrapper/wrapper.py index d69bcff6c..4f0108226 100644 --- a/docker-app/worker_wrapper/wrapper.py +++ b/docker-app/worker_wrapper/wrapper.py @@ -157,20 +157,14 @@ def run(self): feedback["error_stack"] = "" try: - self.job.output = output.decode("utf-8") - self.job.feedback = feedback - self.job.status = Job.Status.FAILED - self.job.save(update_fields=["output", "feedback", "status"]) - logger.info( - "Set job status to `failed` due to being killed by the docker engine.", - ) + self.job.refresh_from_db() except Exception as err: logger.error( "Failed to update job status, probably does not exist in the database.", exc_info=err, ) - # No further action required, probably received by wrapper's autoclean mechanism when the `Project` is deleted - return + # No further action required, probably received by wrapper's autoclean mechanism when the `Project` is deleted + return elif exit_code == TIMEOUT_ERROR_EXIT_CODE: feedback["error"] = "Worker timeout error." feedback["error_type"] = "TIMEOUT" @@ -291,6 +285,12 @@ def _run_docker( f"{settings.QFIELDCLOUD_LIBQFIELDSYNC_VOLUME_PATH}:/libqfieldsync:ro" ) + # used for local development of QFieldCloud + if settings.QFIELDCLOUD_QFIELDCLOUD_SDK_VOLUME_PATH: + volumes.append( + f"{settings.QFIELDCLOUD_QFIELDCLOUD_SDK_VOLUME_PATH}:/qfieldcloud-sdk-python:ro" + ) + # `docker_started_at`/`docker_finished_at` tracks the time spent on docker only self.job.docker_started_at = timezone.now() self.job.save(update_fields=["docker_started_at"]) @@ -543,13 +543,21 @@ def after_docker_run(self) -> None: def after_docker_exception(self) -> None: Delta.objects.filter( id__in=self.delta_ids, - ).update(last_status=Delta.Status.ERROR) + ).update( + last_status=Delta.Status.ERROR, + last_feedback=None, + last_modified_pk=None, + last_apply_attempt_at=self.job.started_at, + last_apply_attempt_by=self.job.created_by, + ) ApplyJobDelta.objects.filter( apply_job_id=self.job_id, delta_id__in=self.delta_ids, ).update( status=Delta.Status.ERROR, + feedback=None, + modified_pk=None, ) diff --git a/docker-compose.override.local.yml b/docker-compose.override.local.yml index 1bfd7911b..048efbcb6 100644 --- a/docker-compose.override.local.yml +++ b/docker-compose.override.local.yml @@ -26,6 +26,7 @@ services: - ${DEBUG_DEBUGPY_WORKER_WRAPPER_PORT:-5679}:5679 environment: QFIELDCLOUD_LIBQFIELDSYNC_VOLUME_PATH: ${QFIELDCLOUD_LIBQFIELDSYNC_VOLUME_PATH} + QFIELDCLOUD_QFIELDCLOUD_SDK_VOLUME_PATH: ${QFIELDCLOUD_QFIELDCLOUD_SDK_VOLUME_PATH} volumes: # mount the source for live reload - ./docker-app/qfieldcloud:/usr/src/app/qfieldcloud @@ -69,6 +70,8 @@ services: volumes: # allow local development for `libqfieldsync` if host directory present; requires `PYTHONPATH=/libqfieldsync:${PYTHONPATH}` - ./docker-qgis/libqfieldsync:/libqfieldsync:ro + # allow local development for `qfieldcloud-sdk-python` if host directory present; requires `PYTHONPATH=/qfieldcloud-sdk-python:${PYTHONPATH}` + - ./docker-qgis/qfieldcloud-sdk-python:/qfieldcloud-sdk-python:ro geodb: image: postgis/postgis:12-3.0 diff --git a/docker-compose.override.test.yml b/docker-compose.override.test.yml index fb4c8823d..2a43a8008 100644 --- a/docker-compose.override.test.yml +++ b/docker-compose.override.test.yml @@ -25,7 +25,7 @@ services: networks: default: # Use a custom driver - name: ${QFIELDCLOUD_DEFAULT_NETWORK} + name: ${QFIELDCLOUD_DEFAULT_NETWORK:-${COMPOSE_PROJECT_NAME}_default} volumes: # We use a different volume, just so that the test_ database diff --git a/docker-compose.yml b/docker-compose.yml index 479d5289e..6898acbb5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,7 +48,7 @@ services: STORAGE_BUCKET_NAME: ${STORAGE_BUCKET_NAME} STORAGE_REGION_NAME: ${STORAGE_REGION_NAME} STORAGE_ENDPOINT_URL: ${STORAGE_ENDPOINT_URL} - QFIELDCLOUD_DEFAULT_NETWORK: ${QFIELDCLOUD_DEFAULT_NETWORK} + QFIELDCLOUD_DEFAULT_NETWORK: ${QFIELDCLOUD_DEFAULT_NETWORK:-${COMPOSE_PROJECT_NAME}_default} GEODB_HOST: ${GEODB_HOST} GEODB_PORT: ${GEODB_PORT} GEODB_USER: ${GEODB_USER} diff --git a/docker-qgis/Dockerfile b/docker-qgis/Dockerfile index 56df24dcc..d01ec1ec1 100644 --- a/docker-qgis/Dockerfile +++ b/docker-qgis/Dockerfile @@ -1,5 +1,5 @@ # NOTE if the ubuntu version is changed, also change it in `Suites:` in apt sources, and in `QGIS_VERSION` -FROM ubuntu:jammy +FROM ubuntu:noble # Install dependencies needed to add QGIS repository RUN apt update \ @@ -13,7 +13,7 @@ RUN wget -O /etc/apt/keyrings/qgis-archive-keyring.gpg https://download.qgis.org COPY <=3.2.0,<3.3 -typing-extensions>=3.7.4.3,<3.7.5 -tabulate==v0.8.9 +jsonschema>=3.2.0 +typing-extensions>=3 +tabulate>=v0.8.9 sentry-sdk requests>=2.28.1 -qfieldcloud-sdk==0.8.2 +qfieldcloud-sdk==0.8.4 diff --git a/docker-qgis/requirements_libqfieldsync.txt b/docker-qgis/requirements_libqfieldsync.txt index e85c6479c..74f65ccbd 100644 --- a/docker-qgis/requirements_libqfieldsync.txt +++ b/docker-qgis/requirements_libqfieldsync.txt @@ -1 +1 @@ -libqfieldsync @ git+https://github.com/opengisch/libqfieldsync@5ed1f872f2449657dfb91a00e9da48469d62e226 +libqfieldsync @ git+https://github.com/opengisch/libqfieldsync@a082504efb02e58df6ee304f62d15de3ef608ecf diff --git a/scripts/check_envvars.sh b/scripts/check_envvars.sh index c5cc141bf..4a705af2b 100755 --- a/scripts/check_envvars.sh +++ b/scripts/check_envvars.sh @@ -1,3 +1,3 @@ #!/bin/bash -e -python3 scripts/check_envvars.py .env.example --docker-compose-dir . --ignored-varnames CONFIG_PATH DEBUG_DEBUGPY_APP_PORT DEBUG_DEBUGPY_WORKER_WRAPPER_PORT +python3 scripts/check_envvars.py .env.example --docker-compose-dir . --ignored-varnames DEBUG_DEBUGPY_APP_PORT DEBUG_DEBUGPY_WORKER_WRAPPER_PORT diff --git a/scripts/init_letsencrypt.sh b/scripts/init_letsencrypt.sh index 10f03705d..d47ebb9ff 100755 --- a/scripts/init_letsencrypt.sh +++ b/scripts/init_letsencrypt.sh @@ -7,14 +7,7 @@ set -o allexport source .env set +o allexport -CONFIG_PATH="${CONFIG_PATH:-'./conf'}" - -if [ ! -e "docker-nginx/options-ssl-nginx.conf" ] || [ ! -e "docker-nginx/ssl-dhparams.pem" ]; then - echo "### Downloading recommended TLS parameters ..." - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "docker-nginx/options-ssl-nginx.conf" - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "docker-nginx/ssl-dhparams.pem" - echo -fi +QFIELDCLOUD_DIR="$(dirname "$(realpath "$0")")/.." echo "### Requesting Let's Encrypt certificate for $QFIELDCLOUD_HOST ..." domain_args="-d ${QFIELDCLOUD_HOST}" @@ -34,8 +27,8 @@ docker compose run --rm --entrypoint "\ echo echo "### Copy the certificate and key to their final destination ..." -cp ${CONFIG_PATH}/certbot/conf/live/${QFIELDCLOUD_HOST}/fullchain.pem docker-nginx/certs/${QFIELDCLOUD_HOST}.pem -cp ${CONFIG_PATH}/certbot/conf/live/${QFIELDCLOUD_HOST}/privkey.pem docker-nginx/certs/${QFIELDCLOUD_HOST}-key.pem +cp ${QFIELDCLOUD_DIR}/conf/certbot/conf/live/${QFIELDCLOUD_HOST}/fullchain.pem ${QFIELDCLOUD_DIR}/docker-nginx/certs/${QFIELDCLOUD_HOST}.pem +cp ${QFIELDCLOUD_DIR}/conf/certbot/conf/live/${QFIELDCLOUD_HOST}/privkey.pem ${QFIELDCLOUD_DIR}/docker-nginx/certs/${QFIELDCLOUD_HOST}-key.pem echo echo "### Reloading nginx ..."