diff --git a/deploy/compose/.env.enterprise-sso.example b/deploy/compose/.env.enterprise-sso.example index c864c6aa..c3476961 100644 --- a/deploy/compose/.env.enterprise-sso.example +++ b/deploy/compose/.env.enterprise-sso.example @@ -14,3 +14,7 @@ CF_PROVIDER_ADMIN_STARTUP_SYNC_ENABLED=false # Optional: require one known business administrator account in the signed roster. # This is a health assertion only; it never grants privileges by username alone. CF_PROVIDER_ADMIN_EXPECTED_ACCOUNT= + +# Enterprise org/role catalog sync (existing cf-worker, every 10 minutes). +# Does not create users or refresh GroupUser during ordinary permission checks. +CF_DIRECTORY_SYNC_INTERVAL_SECONDS=600 diff --git a/deploy/compose/docker-compose.yml b/deploy/compose/docker-compose.yml index 8e7f078f..53a6af3d 100644 --- a/deploy/compose/docker-compose.yml +++ b/deploy/compose/docker-compose.yml @@ -298,6 +298,7 @@ services: CF_PROVIDER_SSO_DIRECTORY: ${CF_PROVIDER_SSO_DIRECTORY:-} CF_SSO_GROUP_OWNER: ${CF_SSO_GROUP_OWNER:-} CF_SSO_SYNC_INTERVAL: ${CF_SSO_SYNC_INTERVAL:-600} + CF_DIRECTORY_SYNC_INTERVAL_SECONDS: ${CF_DIRECTORY_SYNC_INTERVAL_SECONDS:-600} CF_SSO_MAX_REMOVAL_RATIO: ${CF_SSO_MAX_REMOVAL_RATIO:-0.5} CF_SSO_DIRECTORY_STATIC: ${CF_SSO_DIRECTORY_STATIC:-} CF_SERVICE_SSO_DIRECTORY_URL: ${CF_SERVICE_SSO_DIRECTORY_URL:-} @@ -395,24 +396,13 @@ services: condition: service_healthy restart: unless-stopped - # Background worker. Two mutually exclusive jobs, chosen by configuration: - # - # CF_PROVIDER_SEARCH=meilisearch -> the V2 Meilisearch indexer - # (`python3 -m cloudfile_ext.v2.search.worker`, checkpoint - # /shared/search-v2-checkpoint.json). Meilisearch answers queries but never - # indexes CloudFile's derived documents itself, so this process is what - # keeps the index current: it loops every 60s, confirms each index task, - # and a failed batch keeps its checkpoint for retry. - # - # otherwise -> `manage.py cf_worker`, which loops over the registry's - # periodic tasks and exits 0 when none is registered. The V2 runtime - # registers none today -- SSO directory sync is the push trigger - # POST /api/v2.1/admin/cloudfile/directory/entities/sync/ -- so this branch - # is a clean idle exit. - # - # Safe to keep in the default stack: it waits for cloudfile to be healthy, and - # restarts on failure only, so a clean idle exit stays exited instead of - # looping. + # Background worker. The existing cf_worker handles periodic tasks (now + # including a 10-minute enterprise org/role catalog sync). The optional + # Meilisearch indexer can run alongside it in this SAME container when + # enterprise Directory is enabled: no separate scheduler service/table. + # Failures in either long-running process restart the worker container. + # If neither indexing nor periodic tasks are configured, the clean idle + # cf_worker exit stays exited (restart: on-failure). cf-worker: image: ${CLOUDFILE_IMAGE:-cloudfile/cloudfile:14.0.0-cf.0} container_name: cloudfile-worker @@ -422,12 +412,19 @@ services: - > /scripts/create_data_links.sh && if [ "$${CF_PROVIDER_SEARCH:-}" = "meilisearch" ]; then - export DJANGO_SETTINGS_MODULE=seahub.settings && - exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env - python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json; + export DJANGO_SETTINGS_MODULE=seahub.settings; + if [ -n "$${CF_SERVICE_SSO_DIRECTORY_URL:-}" ]; then + /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env python3 /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub/manage.py cf_worker & + periodic_pid=$$!; + /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json & + search_pid=$$!; + trap 'kill "$${periodic_pid}" "$${search_pid}" 2>/dev/null || true' EXIT TERM INT; + wait -n "$${periodic_pid}" "$${search_pid}"; + exit 1; + fi; + exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json; fi; - exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env - python3 /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub/manage.py cf_worker + exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env python3 /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub/manage.py cf_worker volumes: - ./data/seafile:/shared environment: @@ -521,6 +518,7 @@ services: CF_PROVIDER_SSO_DIRECTORY: ${CF_PROVIDER_SSO_DIRECTORY:-} CF_SSO_GROUP_OWNER: ${CF_SSO_GROUP_OWNER:-} CF_SSO_SYNC_INTERVAL: ${CF_SSO_SYNC_INTERVAL:-600} + CF_DIRECTORY_SYNC_INTERVAL_SECONDS: ${CF_DIRECTORY_SYNC_INTERVAL_SECONDS:-600} CF_SSO_MAX_REMOVAL_RATIO: ${CF_SSO_MAX_REMOVAL_RATIO:-0.5} CF_SSO_DIRECTORY_STATIC: ${CF_SSO_DIRECTORY_STATIC:-} CF_SERVICE_SSO_DIRECTORY_URL: ${CF_SERVICE_SSO_DIRECTORY_URL:-} diff --git a/scripts/scripts_cf14.0/cloudfile_bootstrap.py b/scripts/scripts_cf14.0/cloudfile_bootstrap.py index d43f231e..fe56ab03 100644 --- a/scripts/scripts_cf14.0/cloudfile_bootstrap.py +++ b/scripts/scripts_cf14.0/cloudfile_bootstrap.py @@ -219,6 +219,10 @@ def write_cloudfile_settings(): ) body += _settings_block_sso() + # V2 enterprise org/role catalog worker interval; one low-frequency task. + # Malformed values fail startup through the Hub's bounded setting gate. + body += 'CF_DIRECTORY_SYNC_INTERVAL_SECONDS = %r\n' % int( + get_conf('CF_DIRECTORY_SYNC_INTERVAL_SECONDS', '600')) # EAP may use the existing OAuth/SSO bridge with the legacy full # group-synchronizer disabled. When the new one-shot administrator sync is # explicitly enabled, still expose its trusted Directory connection. diff --git a/tests/test_worker_lifecycle.py b/tests/test_worker_lifecycle.py index 63d513e9..11e7cd9a 100644 --- a/tests/test_worker_lifecycle.py +++ b/tests/test_worker_lifecycle.py @@ -1,7 +1,7 @@ """The cf-worker service must not restart forever after a clean idle exit. `manage.py cf_worker` exits 0 when no capability registered a periodic task, and -the V2 runtime registers none today. With `restart: unless-stopped` that clean +the standalone V2 runtime registers none without a Directory. With `restart: unless-stopped` that clean exit becomes an endless restart loop (observed ExitCode=0, RestartCount 455+ on minio157), so the policy must be `on-failure`. @@ -70,6 +70,13 @@ def test_worker_runs_the_meilisearch_indexer_when_it_is_the_provider(self): self.assertIn("cloudfile_ext.v2.search.worker", command) self.assertIn("/shared/search-v2-checkpoint.json", command) + def test_meili_mode_keeps_directory_catalog_scheduling(self): + command = "\\n".join(self.block) + self.assertIn('CF_SERVICE_SSO_DIRECTORY_URL', command) + self.assertIn('periodic_pid=$!', command) + self.assertIn('wait -n "${periodic_pid}" "${search_pid}"', command) + self.assertIn('CF_DIRECTORY_SYNC_INTERVAL_SECONDS: ${CF_DIRECTORY_SYNC_INTERVAL_SECONDS:-600}', command) + def test_worker_stays_in_the_default_stack(self): self.assertEqual([], service_values(self.block, "profiles"))