From 2d43a39acb4337f8326360a7b0c53ee2d49a6e2d Mon Sep 17 00:00:00 2001 From: FYWinds Date: Wed, 8 Apr 2026 22:23:52 -0400 Subject: [PATCH 1/5] feat: enhance deployment configuration and versioning with WCS_VERSION_SUFFIX --- README.md | 286 +++++++++++++++++++++---- app/schemas/constants.py | 5 +- deploy/overlays/dev/kustomization.yaml | 10 + 3 files changed, 254 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 2109cf3..ff526fc 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,240 @@ -## Deploying - -First create the necessary secrets for database and admin token access. Replace the placeholders with your actual values. -```bash -kubectl -n wynnsource-dev create secret generic wynnsource-secrets \ - --from-literal=WCS_ADMIN_TOKEN='' -``` -Or you can use sealed secrets for better security. - -Then use this as an example to deploy the application using ArgoCD. -```yaml -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: wynnsource-dev - namespace: argocd -spec: - project: default - source: - repoURL: git@github.com:WynnSource/WynnSourceServer.git - targetRevision: dev - path: deploy - destination: - server: https://kubernetes.default.svc - namespace: wynnsource-dev - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true -``` -You have to add your ingress configuration as well, -also make sure there is the `X-Real-IP` header from the ingress controller for proper client IP logging and rate limiting. - -## License - -`Copyright (C) <2026> ` - -This project is licensed under the GNU Affero General Public License v3.0 (AGPL-v3) with the following [**Exception**](#exception-for-generated-client-code). - -### Exception for Generated Client Code -As a special exception to the AGPL-v3, the copyright holders of this library give you permission to generate, use, distribute, and license the client libraries (SDKs) generated from this project's API specifications (e.g., OpenAPI/Swagger documents, Protocol Buffers, GraphQL schemas) under any license of your choice, including proprietary licenses. This exception does not apply to the backend logic itself. - -### Reason -We've considered the implications of the AGPL-v3 and have decided to apply it to the backend logic of this project to ensure that any modifications to the server-side code are shared with the community. However, we recognize that client libraries generated from our API specifications may be used in a wide variety of applications, including minecraft mods, which may not be compatible with the AGPL-v3. By granting this exception, we aim to encourage the use of our API and allow developers to create client libraries without worrying about licensing issues, while still ensuring that contributions to the server-side code are shared with the community. \ No newline at end of file +
+ +# WynnSourceServer + +The server component of [WynnSource](https://github.com/WynnSource) — a Wynncraft crowdsourcing mod. + +[![Python 3.12+](https://img.shields.io/badge/python-3.12+-3776AB?logo=python&logoColor=white)](https://www.python.org/) [![FastAPI](https://img.shields.io/badge/FastAPI-009688?logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/) [![Protobuf](https://img.shields.io/badge/Protocol%20Buffers-4285F4?logo=google&logoColor=white)](https://protobuf.dev/) [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) + +
+ +--- + +## Prerequisites + +- Python 3.12+ +- [uv](https://docs.astral.sh/uv/) — Python package manager +- [buf](https://buf.build/) — Protocol Buffers toolchain +- A PostgreSQL database +- A Redis instance + +## Development Setup + +```bash +# Clone with submodules +git clone --recurse-submodules https://github.com/WynnSource/WynnSourceServer.git +cd WynnSourceServer + +# Generate protobuf code +buf generate --template buf.gen.yaml + +# Install dependencies +uv sync + +# Run database migrations +uv run alembic upgrade head + +# Start the development server +uv run fastapi dev +``` + +### Environment Variables + +| Variable | Description | Default | +|---|---|---| +| `POSTGRES_HOST` | PostgreSQL host | `localhost` | +| `POSTGRES_PORT` | PostgreSQL port | `5432` | +| `POSTGRES_USER` | PostgreSQL user | `postgres` | +| `POSTGRES_PASSWORD` | PostgreSQL password | `postgres` | +| `POSTGRES_DB` | PostgreSQL database name | `wcs_db` | +| `REDIS_HOST` | Redis host | `localhost` | +| `REDIS_PORT` | Redis port | `6379` | +| `WCS_ADMIN_TOKEN` | Admin API token | None | +| `LEVEL` | Log level | `DEBUG` | +| `BETA_ALLOWED_VERSIONS` | Comma-separated allowed mod versions | `` | + +## Deploying to Kubernetes + +WynnSourceServer uses [Kustomize](https://kustomize.io/) overlays for deployment and [ArgoCD](https://argo-cd.readthedocs.io/) with [Image Updater](https://argocd-image-updater.readthedocs.io/) for GitOps. + +### Directory Structure + +``` +deploy/ + base/ # Shared manifests + deployment.yaml # App deployment with health probes + service.yaml # ClusterIP service on port 8000 + postgres.yaml # CNPG PostgreSQL cluster + redis.yaml # Redis instance (via Redis Operator) + migration-job.yaml # Alembic migration (ArgoCD sync hook) + kustomization.yaml + overlays/ + dev/ # Dev environment (namespace: wynnsource-dev) + kustomization.yaml + prod/ # Prod environment (namespace: wynnsource, replicas: 2) + kustomization.yaml +``` + +### Cluster Prerequisites + +The following operators must be installed in the cluster: + +- [CloudNativePG](https://cloudnative-pg.io/) — PostgreSQL operator +- [Redis Operator](https://github.com/OT-CONTAINER-KIT/redis-operator) — Redis operator +- [ArgoCD Image Updater](https://argocd-image-updater.readthedocs.io/) — Automatic image updates + +### Step 1: Create Secrets + +Create the application secrets in the target namespace. Using [Sealed Secrets](https://sealed-secrets.netlify.app/) is recommended. + +```bash +kubectl -n create secret generic wynnsource-secrets \ + --from-literal=WCS_ADMIN_TOKEN='' +``` + +### Step 2: Create ConfigMap + +```bash +kubectl -n create configmap wynnsource-config \ + --from-literal=LEVEL='INFO' \ + --from-literal=BETA_ALLOWED_VERSIONS='0.2.6, 0.2.7' +``` + +### Step 3: Create ArgoCD Application + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: wynnsource + namespace: argocd + annotations: + argocd-image-updater.argoproj.io/image-list: app=ghcr.io/wynnsource/wynnsource-server + argocd-image-updater.argoproj.io/app.update-strategy: newest-build + argocd-image-updater.argoproj.io/app.allow-tags: "regexp:^dev-" + argocd-image-updater.argoproj.io/app.ignore-tags: "latest,dev-latest" +spec: + project: default + source: + repoURL: https://github.com/WynnSource/WynnSourceServer.git + targetRevision: dev # or master for production + path: deploy/overlays/dev # or deploy/overlays/prod + destination: + server: https://kubernetes.default.svc + namespace: wynnsource-dev # or wynnsource + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true +``` + +### Step 4: Configure Ingress + +Add an IngressRoute (or Ingress) pointing to the `wynnsource-server` service on port 8000. Make sure the `X-Real-IP` header is forwarded from the ingress controller for proper client IP logging and rate limiting. + +## Using the Schema (Protobuf) + +WynnSourceServer uses [Protocol Buffers](https://protobuf.dev/) to define item encoding schemas. The `.proto` definitions live in the [`schema`](https://github.com/WynnSource/schema) submodule under `schema/proto/wynnsource/`. + +### Generating Python Code + +#### With buf (recommended) + +Install [buf](https://buf.build/docs/installation), then from the repository root: + +```bash +buf generate --template buf.gen.yaml +``` + +This generates Python protobuf modules into `generated/wynnsource/` based on: + +```yaml +# buf.gen.yaml +version: v2 +plugins: + - remote: buf.build/protocolbuffers/python:v33.5 + out: generated + - remote: buf.build/protocolbuffers/pyi:v33.5 + out: generated +inputs: + - directory: schema/proto +``` + +#### With protoc + +```bash +protoc \ + --proto_path=schema/proto \ + --python_out=generated \ + --pyi_out=generated \ + schema/proto/wynnsource/**/*.proto +``` + +### Using Generated Code + +Install the `protobuf` runtime, then import the generated modules: + +```bash +pip install protobuf +``` + +```python +from wynnsource.item.gear_pb2 import IdentifiedGear +from wynnsource.item.consumable_pb2 import Consumable +from wynnsource.common.enums_pb2 import Rarity + +# Create an item +gear = IdentifiedGear() +gear.name = "Cataclysm" + +# Serialize to bytes +data = gear.SerializeToString() + +# Deserialize from bytes +parsed = IdentifiedGear() +parsed.ParseFromString(data) +``` + +### Using as a Workspace Package + +In this repository, the generated code is a [uv workspace](https://docs.astral.sh/uv/concepts/workspaces/) package called `wynnsource-proto`. After running `buf generate` and `uv sync`, you can import directly: + +```python +from wynnsource.item.wynn_source_item_pb2 import WynnSourceItem +``` + +### Mappings + +The schema repository includes JSON mapping files: + +- `schema/mapping/identification.json` — Identification ID mappings +- `schema/mapping/shiny.json` — Shiny stat mappings + +### Generating for Other Languages + +buf supports many languages. See the [buf plugin registry](https://buf.build/plugins) for available plugins. Example for TypeScript: + +```yaml +# buf.gen.yaml +version: v2 +plugins: + - remote: buf.build/connectrpc/es + out: gen/ts +inputs: + - directory: schema/proto +``` + +## License + +This project is licensed under the [GNU Affero General Public License v3.0 (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0) with the following exception. + +### Exception for Generated Client Code + +As a special exception, the copyright holders grant permission to generate, use, distribute, and license client libraries and SDKs produced from this project's API specifications (including OpenAPI documents, Protocol Buffer definitions, and GraphQL schemas) under any license of your choice, including proprietary licenses. This exception applies solely to the generated client code — the server-side source code remains subject to the AGPL-3.0 in full. + +### Rationale + +The AGPL-3.0 ensures that improvements to the server are shared with the community. However, client libraries derived from our API specifications are commonly embedded in Minecraft mods and other applications whose licenses may be incompatible with the AGPL. This exception removes that friction: developers can freely build and ship clients against our API without licensing concerns, while contributions to the backend itself continue to benefit everyone. diff --git a/app/schemas/constants.py b/app/schemas/constants.py index 33c0d61..55aaf39 100644 --- a/app/schemas/constants.py +++ b/app/schemas/constants.py @@ -1,4 +1,5 @@ import enum +import os import tomllib @@ -19,7 +20,9 @@ def get_media_type(cls, media_type: str) -> str: pyproject = tomllib.load(f) __NAME__ = "WynnSource Server" -__VERSION__: str = pyproject["project"]["version"] +_base_version: str = pyproject["project"]["version"] +_version_suffix = os.environ.get("WCS_VERSION_SUFFIX", "") +__VERSION__: str = f"{_base_version}{_version_suffix}" if _version_suffix else _base_version __DESCRIPTION__: str = pyproject["project"]["description"] __REVISION__ = 2 diff --git a/deploy/overlays/dev/kustomization.yaml b/deploy/overlays/dev/kustomization.yaml index 67a22ab..e159fbd 100644 --- a/deploy/overlays/dev/kustomization.yaml +++ b/deploy/overlays/dev/kustomization.yaml @@ -6,3 +6,13 @@ resources: images: - name: ghcr.io/wynnsource/wynnsource-server newTag: dev-latest +patches: + - target: + kind: Deployment + name: wynnsource-server + patch: | + - op: add + path: /spec/template/spec/containers/0/env + value: + - name: WCS_VERSION_SUFFIX + value: "-beta" From 669e9bcceb12c14535616cf4750fde5f54bcdc0b Mon Sep 17 00:00:00 2001 From: FYWinds Date: Wed, 8 Apr 2026 22:33:53 -0400 Subject: [PATCH 2/5] feat: add labels for instance identification in kustomization files --- deploy/overlays/dev/kustomization.yaml | 4 ++++ deploy/overlays/prod/kustomization.yaml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/deploy/overlays/dev/kustomization.yaml b/deploy/overlays/dev/kustomization.yaml index e159fbd..1920e05 100644 --- a/deploy/overlays/dev/kustomization.yaml +++ b/deploy/overlays/dev/kustomization.yaml @@ -6,6 +6,10 @@ resources: images: - name: ghcr.io/wynnsource/wynnsource-server newTag: dev-latest +labels: + - pairs: + app.kubernetes.io/instance: wynnsource-dev + includeSelectors: false patches: - target: kind: Deployment diff --git a/deploy/overlays/prod/kustomization.yaml b/deploy/overlays/prod/kustomization.yaml index 6602c1a..602396f 100644 --- a/deploy/overlays/prod/kustomization.yaml +++ b/deploy/overlays/prod/kustomization.yaml @@ -6,6 +6,10 @@ resources: images: - name: ghcr.io/wynnsource/wynnsource-server newTag: latest +labels: + - pairs: + app.kubernetes.io/instance: wynnsource-prod + includeSelectors: false patches: - target: kind: Deployment From 0beb3f746e532bbf56f13b82cf34a2486efe4ec5 Mon Sep 17 00:00:00 2001 From: FYWinds Date: Wed, 8 Apr 2026 22:49:34 -0400 Subject: [PATCH 3/5] fix: correct patch path for WCS_VERSION_SUFFIX in kustomization.yaml --- deploy/overlays/dev/kustomization.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/overlays/dev/kustomization.yaml b/deploy/overlays/dev/kustomization.yaml index 1920e05..870e322 100644 --- a/deploy/overlays/dev/kustomization.yaml +++ b/deploy/overlays/dev/kustomization.yaml @@ -16,7 +16,7 @@ patches: name: wynnsource-server patch: | - op: add - path: /spec/template/spec/containers/0/env + path: /spec/template/spec/containers/0/env/- value: - - name: WCS_VERSION_SUFFIX - value: "-beta" + name: WCS_VERSION_SUFFIX + value: "-beta" From cdf08a9ad1f81c56ea1e185b7d304dc6a85e06b6 Mon Sep 17 00:00:00 2001 From: FYWinds Date: Thu, 9 Apr 2026 09:42:19 -0400 Subject: [PATCH 4/5] fix: updated gambit region and etc. --- app/module/raid/config.py | 10 +++++----- app/module/raid/router.py | 8 ++------ app/module/raid/schema.py | 4 ---- app/module/raid/service.py | 11 +++-------- 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/app/module/raid/config.py b/app/module/raid/config.py index 49c8cc5..cda786e 100644 --- a/app/module/raid/config.py +++ b/app/module/raid/config.py @@ -6,6 +6,7 @@ GAMBIT_COUNT = 4 GAMBIT_SEPARATOR = "|" +GAMBIT_REGION = "global" FUZZY_WINDOW = timedelta(minutes=30) CONSENSUS_THRESHOLD = 0.6 @@ -19,17 +20,16 @@ class GambitRotation: def get_gambit_rotation(time: datetime, shift: int = 0) -> GambitRotation: """Get the daily gambit rotation window for the given timestamp. - Gambits rotate daily at EST/EDT midnight (00:00 America/New_York). + Gambits rotate daily at EST/EDT noon (12:00 America/New_York). """ if time.tzinfo is None: raise ValueError("The 'time' parameter must be timezone-aware.") local_time = time.astimezone(SERVER_TZ) - # Today's reset at midnight EST - today_reset = datetime.combine( - local_time.date(), datetime.min.time(), tzinfo=SERVER_TZ - ) + # Today's reset at noon EST + today_reset = datetime.combine(local_time.date(), datetime.min.time(), tzinfo=SERVER_TZ) + today_reset += timedelta(hours=12) if local_time < today_reset: today_reset -= timedelta(days=1) diff --git a/app/module/raid/router.py b/app/module/raid/router.py index d6d8cff..490303e 100644 --- a/app/module/raid/router.py +++ b/app/module/raid/router.py @@ -8,7 +8,6 @@ from app.core.rate_limiter import ip_based_key_func, user_based_key_func from app.core.router import DocedAPIRoute from app.core.security.auth import UserDep -from app.module.pool.schema import RaidRegion from app.schemas.enums import ApiTag from app.schemas.response import EMPTY_RESPONSE, EmptyResponse, WCSResponse @@ -47,11 +46,10 @@ async def submit_gambit_data(data: list[GambitSubmissionSchema], user: UserDep) return EMPTY_RESPONSE -@RaidRouter.get("/gambit/{region}", summary="Get Current Gambit Consensus") +@RaidRouter.get("/gambit", summary="Get Current Gambit Consensus") @metadata.rate_limit(limit=10, period=60, key_func=ip_based_key_func) @metadata.cached(expire=120) async def get_gambit_by_region( - region: RaidRegion, session: SessionDep, ) -> WCSResponse[GambitConsensusResponse]: """ @@ -59,11 +57,10 @@ async def get_gambit_by_region( """ try: rotation = get_gambit_rotation(datetime.datetime.now(tz=datetime.UTC)) - result = await get_gambit_consensus(session, region, rotation.start) + result = await get_gambit_consensus(session, rotation.start) if result is None: data = GambitConsensusResponse( - region=region, rotation_start=rotation.start, rotation_end=rotation.end, gambits=[], @@ -72,7 +69,6 @@ async def get_gambit_by_region( else: pairs, confidence = result data = GambitConsensusResponse( - region=region, rotation_start=rotation.start, rotation_end=rotation.end, gambits=[ diff --git a/app/module/raid/schema.py b/app/module/raid/schema.py index d954d07..6e893fb 100644 --- a/app/module/raid/schema.py +++ b/app/module/raid/schema.py @@ -2,8 +2,6 @@ from pydantic import BaseModel, Field, model_validator -from app.module.pool.schema import RaidRegion - from .config import GAMBIT_COUNT @@ -13,7 +11,6 @@ class GambitEntry(BaseModel): class GambitSubmissionSchema(BaseModel): - region: RaidRegion client_timestamp: datetime mod_version: str gambits: list[GambitEntry] = Field( @@ -37,7 +34,6 @@ class GambitConsensusEntry(BaseModel): class GambitConsensusResponse(BaseModel): - region: str rotation_start: datetime rotation_end: datetime gambits: list[GambitConsensusEntry] diff --git a/app/module/raid/service.py b/app/module/raid/service.py index c546a82..1ca0d01 100644 --- a/app/module/raid/service.py +++ b/app/module/raid/service.py @@ -7,10 +7,9 @@ from app.core.db import get_session from app.core.scheduler import SCHEDULER from app.core.security.model import User -from app.module.pool.schema import RaidRegion from app.module.pool.service import calculate_submission_weight -from .config import FUZZY_WINDOW, GAMBIT_COUNT, GAMBIT_SEPARATOR, get_gambit_rotation +from .config import FUZZY_WINDOW, GAMBIT_COUNT, GAMBIT_REGION, GAMBIT_SEPARATOR, get_gambit_rotation from .model import GambitRepository, GambitSubmission, GambitSubmissionRepository from .schema import GambitSubmissionSchema @@ -26,7 +25,7 @@ async def submit_gambit_data(session: AsyncSession, data: GambitSubmissionSchema rotation = get_gambit_rotation(data.client_timestamp) gambit = await gambit_repo.get_or_create_gambit( - region=data.region.value, + region=GAMBIT_REGION, rotation=rotation, ) @@ -104,21 +103,17 @@ async def compute_gambit_consensus(): slot_confidences.append(best_weight / total_slot_weight if total_slot_weight > 0 else 0.0) gambit.consensus_data = consensus_data - gambit.confidence = round( - sum(slot_confidences) / len(slot_confidences) if slot_confidences else 0.0, 4 - ) + gambit.confidence = round(sum(slot_confidences) / len(slot_confidences) if slot_confidences else 0.0, 4) gambit.needs_recalc = False async def get_gambit_consensus( session: AsyncSession, - region: RaidRegion, rotation_start: datetime.datetime, ) -> tuple[list[tuple[str, str]], float] | None: """Returns ([(name, description), ...], confidence) or None if no data.""" gambit_repo = GambitRepository(session) gambits = await gambit_repo.list_gambits( - region=region.value, rotation_start=rotation_start, ) From d0416988a2f48a6df71b4559e9aedcc41fb06b6e Mon Sep 17 00:00:00 2001 From: FYWinds Date: Thu, 9 Apr 2026 09:43:44 -0400 Subject: [PATCH 5/5] chore: bump version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f8c60e0..80bbfb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "wynnsource_server" -version = "0.1.0" +version = "0.1.1" description = "The server component of WynnSource - a Wynncraft Crowdsourcing Mod." readme = "README.md" requires-python = ">=3.12, <4.0" diff --git a/uv.lock b/uv.lock index 1059a2d..ae0fe70 100644 --- a/uv.lock +++ b/uv.lock @@ -1604,7 +1604,7 @@ requires-dist = [ [[package]] name = "wynnsource-server" -version = "0.1.0" +version = "0.1.1" source = { virtual = "." } dependencies = [ { name = "alembic" },