الفصل 13b العملاء والعمل الجماعي وCLI الإدارة: #pConfigFile

الإعداد كشيفرة: التصدير والتخطيط والتطبيق وخط معالجة Git

حالة الخادم بالكامل كملفات YAML في Git: المستأجرون، والنطاقات (محددات DKIM وMTA-STS والتوجيه)، وصناديق البريد مع الأسماء المستعارة وإعادة التوجيه والرد التلقائي، وقوائم التوزيع، وصناديق البريد المشتركة، والغرف، وقائمة العناوين العامة (GAL) وقواعد الموارد، والمشاركات والتفويض، وSMTP، والمضيف الذكي (Smart Host)، والمسارات، وقواعد النقل، والحماية من البريد العشوائي والفيروسات، وجدار الحماية والحظر التلقائي، وTLS، ومصادر الهوية، ومفاتيح API (البيانات الوصفية فقط)، وWebhooks، والإشعارات، وتنبيهات التشغيل، وPOP3، والملفات، والحجز، والأرشيف، وDNS failover. يعرض nextxch-cli config plan، على غرار terraform plan، ما سيُنشأ وما سيُغيَّر وما سيُحذف؛ ويتحقق apply من كل شيء أولًا ثم يطبّقه بترتيب ثابت. في لوحة الإدارة: System > Configuration as a file.

نصيحة تشغيلية وملاحظة حول البنية

لا تدخل الأسرار إلى الملف أبدًا ولا تُصدَّر أبدًا: تُكتب كلمات المرور والرموز والمفاتيح بالصيغة secretRef: {env: NAME} أو secretRef: {file: path} وتُحلَّل وقت التطبيق (بواسطة nextxch-cli على مشغّل CI؛ وعلى الخادم فقط متغيرات NEXTXCH_SECRET_* والملفات الموجودة تحت <data>/secrets/). وتبقى مفاتيح DKIM والشهادات وتجزئات كلمات المرور على الخادم. ميزة من Business مثل API v1؛ أما JSON Schema فمجاني.

1. تنسيق الملف: apiVersion وkind وmetadata.name وspec

ملف واحد يضم عدة مستندات (مفصولة بـ ---) أو مجلد يضم ملفًا واحدًا لكل مجال (config export --split). تبقى الحقول غير الموجودة في الملف كما هي على الخادم؛ أما القوائم (الأسماء المستعارة، الأعضاء) فتُعامَل ككل.

المرجع / الإعداد
# yaml-language-server: $schema=https://nextxch.com/schemas/nextxch-config-v1.schema.json
apiVersion: nextxch/v1
kind: Manifest            # optional: areas in which --prune may delete
metadata: {name: nextxch}
spec: {areas: [domains, accounts, sharing, transport]}
---
apiVersion: nextxch/v1
kind: Domain
metadata: {name: company.com}
spec:
  tenant: default
  catchAll: info@company.com
  dkim: {mode: dual, rsaSelector: s2026, ed25519Selector: ed2026}   # keys stay on the server
  mtaSts: {mode: enforce, maxAgeSeconds: 604800, mxHosts: [mail.company.com]}
---
apiVersion: nextxch/v1
kind: Account
metadata: {name: anna.smith@company.com}
spec:
  displayName: Anna Smith
  role: User                      # User, TenantAdmin, ServerAdmin, ComplianceAuditor
  quotaMb: 10240
  aliases: [anna@company.com]
  forwarding: {address: anna@private.example, keepCopy: true}
  outOfOffice: {enabled: false}
  password: {secretRef: {env: ANNA_PASSWORD}}   # never the password itself
---
apiVersion: nextxch/v1
kind: Account
metadata: {name: info@company.com}
spec: {type: shared, displayName: Info mailbox}   # user, shared, room, equipment
---
apiVersion: nextxch/v1
kind: Share
metadata: {name: "info@company.com/mailbox -> anna.smith@company.com"}
spec: {permissions: [read, write, sendAs]}
---
apiVersion: nextxch/v1
kind: SmtpSettings
metadata: {name: server}             # server-wide settings are always named "server"
spec:
  outboundSmartHost: smtp.provider.example
  outboundSmartPort: 587
  smartHostPassword: {secretRef: {file: secrets/smarthost.txt}}

2. سير العمل مع nextxch-cli

صدّروا والتزموا (commit) أولًا، ثم أجروا التغييرات عبر طلبات السحب (pull requests). مثال على مخرجات plan:

المرجع / الإعداد
export NEXTXCH_URL=https://mail.company.com
export NEXTXCH_API_KEY=smk_...            # key with config.read or config.write
nextxch-cli config export --split nextxch/
nextxch-cli config validate -f nextxch/    # offline: kinds and fields
nextxch-cli config plan -f nextxch/

  ~ Domain company.com
      mtaSts.mode: "testing" -> "enforce"
  ~ Account anna.smith@company.com
      quotaMb: 10240 -> 20480
      aliases: ["anna@company.com"] -> ["a.smith@company.com","anna@company.com"]
      ~ password: (secret changed)
  + Account info@company.com
      type: "shared"
      displayName: "Info mailbox"

Not in the file (1), deleted only with --prune:
    Account old@company.com

Plan: 1 to create, 2 to update, 0 to delete, 41 unchanged.
# exit code 2 = changes

nextxch-cli config apply -f nextxch/ --prune --yes
nextxch-cli config history

3. GitHub Actions: الخطة عند طلب الدمج (Pull Request)، والتطبيق عند الدمج

الملف الكامل مع خطوة التعليق: deploy/gitops/github-actions.yml في المستودع. ويأتي مفتاح API من أسرار المستودع، وليس من الملف أبدًا.

المرجع / الإعداد
name: nextxch config
on:
  pull_request: {paths: ["nextxch/**"]}
  push: {branches: [main], paths: ["nextxch/**"]}
permissions: {contents: read, pull-requests: write}
concurrency: {group: nextxch-config, cancel-in-progress: false}
env: {NEXTXCH_IMAGE: "nextxch/server:latest"}
jobs:
  plan:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Plan
        env:
          NEXTXCH_URL: ${{ vars.NEXTXCH_URL }}
          NEXTXCH_API_KEY: ${{ secrets.NEXTXCH_CONFIG_READ_KEY }}
        run: |
          set +e
          docker run --rm -e NEXTXCH_URL -e NEXTXCH_API_KEY -e NO_COLOR=1 -v "$PWD:/work" -w /work \
            --entrypoint nextxch-cli "$NEXTXCH_IMAGE" config plan -f nextxch/ --prune > plan.txt 2>&1
          code=$?; cat plan.txt; [ "$code" -ne 1 ]
      # danach: actions/github-script postet plan.txt als Kommentar (siehe deploy/gitops)
  apply:
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - env:
          NEXTXCH_URL: ${{ vars.NEXTXCH_URL }}
          NEXTXCH_API_KEY: ${{ secrets.NEXTXCH_CONFIG_WRITE_KEY }}
          SMARTHOST_PASSWORD: ${{ secrets.SMARTHOST_PASSWORD }}
        run: |
          docker run --rm -e NEXTXCH_URL -e NEXTXCH_API_KEY -e SMARTHOST_PASSWORD -v "$PWD:/work" -w /work \
            --entrypoint nextxch-cli "$NEXTXCH_IMAGE" config apply -f nextxch/ --prune --yes

4. GitLab CI: الخطة كملاحظة على طلب الدمج، والتطبيق على الفرع الافتراضي

الملف الكامل: deploy/gitops/gitlab-ci.yml. المفاتيح متغيرات CI/CD مقنّعة ومحمية؛ ويمنع resource_group تنفيذ عمليتَي apply في الوقت نفسه.

المرجع / الإعداد
stages: [validate, plan, apply]
variables: {CONFIG_DIR: nextxch, NO_COLOR: "1"}
.nextxch-cli:
  image: {name: "nextxch/server:latest", entrypoint: [""]}
validate:
  extends: .nextxch-cli
  stage: validate
  script: [nextxch-cli config validate -f "$CONFIG_DIR"]
plan:
  extends: .nextxch-cli
  stage: plan
  rules: [{if: '$CI_PIPELINE_SOURCE == "merge_request_event"'}]
  variables: {NEXTXCH_API_KEY: $NEXTXCH_CONFIG_READ_KEY}
  script:
    - set +e
    - nextxch-cli config plan -f "$CONFIG_DIR" --prune > plan.txt 2>&1; code=$?
    - cat plan.txt; test "$code" -ne 1
  artifacts: {when: always, paths: [plan.txt]}
# plan-comment: curl auf /merge_requests/:iid/notes mit plan.txt (siehe deploy/gitops)
apply:
  extends: .nextxch-cli
  stage: apply
  resource_group: nextxch-config
  environment: production
  rules: [{if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'}]
  variables: {NEXTXCH_API_KEY: $NEXTXCH_CONFIG_WRITE_KEY}
  script: [nextxch-cli config apply -f "$CONFIG_DIR" --prune --yes]

5. JSON Schema في المحرر واستدعاء REST

تتحقق VS Code (إضافة "YAML" من Red Hat) وبيئات JetBrains من الملفات أثناء الكتابة. كما يوفر كل خادم المخطط على /api/v1/config/schema.

المرجع / الإعداد
# .vscode/settings.json
{
  "yaml.schemas": {
    "https://nextxch.com/schemas/nextxch-config-v1.schema.json": ["nextxch/*.yaml"]
  }
}

# Plan ohne CLI, direkt per REST (YAML als Body):
curl -s -X POST "$NEXTXCH_URL/api/v1/config/plan?prune=true" \
  -H "Authorization: Bearer $NEXTXCH_API_KEY" \
  -H "Content-Type: application/yaml" --data-binary @nextxch.yaml

6. الأسرار في القوائم وACME دون إعادة تشغيل

خطافات MTA مع ترويسة مصادقة وسر HMAC من خط المعالجة، ووجهة تنبيه مع سر webhook، والتحول إلى Let's Encrypt مع DNS-01: عملية apply واحدة، دون إعادة تشغيل.

المرجع / الإعداد
apiVersion: nextxch/v1
kind: MtaFilters
metadata: {name: server}
spec:
  hooks:
    - name: spamcheck
      url: https://hooks.company.com/check
      stages: [data]
      authHeaderName: Authorization
      authHeaderValue: {secretRef: {env: HOOK_TOKEN}}   # in the plan: hooks[spamcheck].authHeaderValue
      hmacSecret: {secretRef: {file: secrets/hook-hmac.txt}}
---
apiVersion: nextxch/v1
kind: OpsAlerts
metadata: {name: server}
spec:
  targets:
    - name: pager
      type: webhook
      address: https://pager.company.com/hook
      secret: {secretRef: {env: PAGER_SECRET}}
---
apiVersion: nextxch/v1
kind: TlsSettings
metadata: {name: server}
spec:
  acme:
    enabled: true
    directoryUrl: https://acme-v02.api.letsencrypt.org/directory
    contactEmail: postmaster@company.com
    challenge: Dns01
    renewBeforeExpiryDays: 30

الإعدادات والقيم الافتراضية وأفضل الممارسات

الإعداد / الموقع الوصف القيمة الافتراضية أفضل ممارسة للإنتاج
config export [--area a,b] [--split ordner/] [-o datei] يصدّر حالة الخادم بشكل حتمي: ترتيب ثابت للمستندات، وقوائم مرتبة، ودون طوابع زمنية. ويعطي التصدير مرتين البايتات نفسها، ولا تُظهر فروق Git إلا التغييرات الفعلية. alle Bereiche استخدموا --split للمستودعات (ملف واحد لكل مجال: domains.yaml وaccounts.yaml ...)؛ فتبقى طلبات الدمج الصغيرة سهلة القراءة.
config plan -f datei|ordner [--prune] يعرض لكل كائن + إنشاء، و~ تحديث (field: old -> new)، و- حذف. رمز الخروج 0 = لا تغييرات، 2 = توجد تغييرات، 1 = خطأ. ولا تظهر الأسرار إلا بصيغة "(secret changed)". ohne Löschen شغّلوه على طلبات الدمج (Pull Requests) وانشروا النتيجة كتعليق (أمثلة أدناه).
config apply -f ... [--prune] [--yes] [--force] [--dry-run] يفحص كل شيء أولًا (الحقول والمراجع والأسرار وقواعد الأمان)، ثم ينشئ ويحدّث بترتيب ثابت (المستأجرون والنطاقات والحسابات والقوائم والقواعد والمشاركات والنقل...)، ويحذف بالترتيب العكسي. وإذا فشلت خطوة، تُلغى الخطوات المنجزة بالفعل. ويُسجَّل كل تغيير في سجل التدقيق. fragt nach مرّروا دائمًا --yes في خطوط المعالجة (Pipelines)؛ وإلا يرفض apply التنفيذ في غياب طرفية. و--dry-run يعادل plan.
--prune und Manifest لا تحدث عمليات الحذف إلا مع --prune، وفقط في المجالات التي يسميها مستند Manifest (دون Manifest: الأنواع الموجودة في الملف فقط). ولا تُحذف إعدادات الخادم أبدًا. aus يكتب التصدير مستند Manifest؛ وإذا كنتم تديرون أجزاءً فقط، فضيّقوا نطاق المجالات.
Schutz: letzter Admin, Domain mit Postfächern تُرفض الخطة التي لا تُبقي أي مسؤول خادم نشط، وكذلك حذف نطاق لا يزال يحتوي على صناديق بريد. ويتجاوز --force ذلك (يُعطَّل النطاق حينئذٍ بدلًا من حذفه). ولا يستطيع مفتاح API إلغاء نفسه. aktiv لا تكتبوا --force بشكل ثابت في خط معالجة أبدًا.
secretRef: {env: NAME} | {file: pfad} مرجع إلى سر بدلًا من قيمته. يحلّه nextxch-cli على المشغّل (أي متغير، أو ملف نسبةً إلى ملف التكوين). ولا يقرأ الخادم نفسه إلا NEXTXCH_SECRET_* والملفات الموجودة تحت <data>/secrets/. ولا تُعد إعادة تطبيق القيمة نفسها تغييرًا. - مرّروا الأسرار من مخزن أسرار GitHub/GitLab إلى المهمة كمتغيرات بيئة.
Scopes config.read / config.write REST: GET /api/v1/config/export، POST /api/v1/config/plan، POST /api/v1/config/apply، GET /api/v1/config/history، GET /api/v1/config/schema (مجاني). المفاتيح: مفاتيح تكامل المستأجر؛ تعمل فقط ما دام منشئها لا يزال مسؤولًا. ترى مفاتيح مسؤول الخادم الخادم بأكمله، ومفاتيح مسؤول المستأجر ذلك المستأجر فقط. يكفي config.read للتصدير والتخطيط، ويتطلب apply الصلاحية config.write. - مفتاحان: config.read للخطط في طلبات الدمج (Pull Requests)، وconfig.write فقط لمهمة التطبيق على main، مع تقييده بعناوين IP الخاصة بالمشغّلات إن أمكن.
Mandanten-Admins يقوم مسؤول المستأجر (عبر الجلسة أو أحد مفاتيحه) بتصدير كائنات مستأجره فقط والتخطيط لها وتطبيقها: Domain (تعديل فقط، دون إنشاء أو حذف)، وAccount، وDistributionList، وShare، وGalVisibilityRule، وResourceAccessRule، وWebhook. تبقى إعدادات الخادم والمستأجرون ومصادر الهوية ومفاتيح API لدى مسؤول الخادم؛ والحسابات ذات الدور ServerAdmin أو ComplianceAuditor خارج نطاقه. يحذف --prune داخل المستأجر نفسه فقط، ويعرض السجل عمليات التطبيق الخاصة به فقط. - للموزعين والأقسام: يحتفظ كل مستأجر بمستودعه الخاص ومفتاحه الخاص. يمكن حذف الحقل "tenant"؛ إذ يُملأ بالمستأجر نفسه.
secretRef in Listen تتلقى عناصر القوائم الأسرار أيضًا عبر secretRef: MtaFilters hooks[].authHeaderValue وhooks[].hmacSecret، وOpsAlerts targets[].secret (HMAC لوجهة webhook). تسمي الخطة العنصر بمعرّفه أو اسمه، مثل hooks[spamcheck].authHeaderValue؛ وتُرفض القيمة الصريحة في القائمة. - تحصل العناصر التي لا تملك معرّفًا على معرّف عند أول apply؛ بعد التصدير احتفظ بالمعرّفات في الملف، فتبقى الأسرار مع العنصر الصحيح عند إعادة ترتيب القائمة.
ACME zur Laufzeit يُطبَّق TlsSettings.acme (enabled وdirectoryUrl وcontactEmail وchallenge Http01/TlsAlpn01/Dns01 وrenewBeforeExpiryDays وreusePrivateKeyForDane) دون إعادة تشغيل: يبقى appsettings.json هو الأساس، وتُحفظ الفروقات في <data>/acme-settings.json وتسري على العنقود بأكمله. يؤدي تغيير عنوان URL للدليل إلى إنشاء حساب ACME جديد عند الطلب التالي. appsettings.json اختبر أولًا باستخدام دليل staging الخاص بـ Let's Encrypt، ثم انتقل إلى الإنتاج.
Arten (kinds) Tenant وDomain وAccount وDistributionList وGalVisibilityRule وResourceAccessRule وShare وSmtpSettings وRoute وTransportRules وSpamFilter وMtaFilters وFirewallRule وTlsSettings وIdentitySource وApiKey وWebhook وNotifications وOpsAlerts وDnsFailover وPop3 وFileStorage وBooking وArchivePolicy. جميع الحقول: JSON Schema. - لا تُنشأ مفاتيح API من الملفات (سرها موجود مرة واحدة فقط)؛ يغيّر الملف التسمية والنطاقات ويلغي المفاتيح باستخدام --prune. إعدادات القاعات والحجوزات متطابقة على كل عقدة في العنقود.