-
Notifications
You must be signed in to change notification settings - Fork 1
Monorepo for nightops #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| # Monorepo Layout (Local Workspace) | ||
|
|
||
| This repo is already packaged as a Python distribution named `nightops` (see `pyproject.toml` at the **repository root**). | ||
|
|
||
| To let you use it inside a monorepo without moving your existing code, we created this local workspace folder: | ||
|
|
||
| - `monorepo/packages/nightops` is a symlink to the repository root (parent of `monorepo/`). | ||
|
|
||
| ## Setup | ||
|
|
||
| From the `monorepo/` directory: | ||
|
|
||
| ```bash | ||
| cd monorepo | ||
| python3 -m venv .venv | ||
| source .venv/bin/activate | ||
|
|
||
| # Install TheNightOps as a package | ||
| pip install -e "packages/nightops[dev]" | ||
| ``` | ||
|
|
||
| ## Using the CLI from the monorepo | ||
|
|
||
| The CLI loads config from a path you pass via `--config` (or from `config/nightops.yaml` relative to your current working directory). | ||
| So, when you run from `monorepo/`, prefer passing an explicit config path: | ||
|
|
||
| ```bash | ||
| nightops verify --config packages/nightops/config/nightops.yaml | ||
| nightops agent run --simple --incident "pod OOMKilled" --config packages/nightops/config/nightops.yaml | ||
| ``` | ||
|
|
||
| Alternative: `cd packages/nightops` before running `nightops ...` so `config/nightops.yaml` resolves naturally. | ||
|
|
||
| ## Adding more packages later | ||
|
|
||
| For additional Python packages, create new folders under `monorepo/packages/<your-package>/` and give each package its own `pyproject.toml`. | ||
|
|
||
| --- | ||
|
|
||
| ## Publish `nightops` (so other repos can install it) | ||
|
|
||
| From the **repository root** (the directory that contains `pyproject.toml`). You can `cd` there explicitly, or: | ||
|
|
||
| ```bash | ||
| REPO_ROOT="$(git rev-parse --show-toplevel)" | ||
| cd "$REPO_ROOT" | ||
| ``` | ||
|
|
||
| ### Step 1: Build artifacts | ||
| ```bash | ||
| # from the TheNightOps repository root | ||
| python3 -m pip install -U pip build twine | ||
| python3 -m build | ||
| ``` | ||
|
|
||
| This generates files in `dist/` (a `.whl` and a `.tar.gz`). | ||
|
|
||
| If `python -m build` fails with an error like `Unknown license exception: 'Commons-Clause-1.0'`, make sure `pyproject.toml` uses: | ||
| `license = { file = "LICENSE" }` | ||
| (this repo has been updated accordingly). | ||
|
|
||
| ### Step 2: Upload to PyPI (or TestPyPI) | ||
|
|
||
| For PyPI: | ||
| ```bash | ||
| twine upload dist/* | ||
| ``` | ||
|
|
||
| For TestPyPI: | ||
| ```bash | ||
| twine upload --repository testpypi dist/* | ||
| ``` | ||
|
|
||
| You need an API token set up in `~/.pypirc` or via `TWINE_USERNAME` / `TWINE_PASSWORD`. | ||
|
|
||
| ### Step 3 (before re-publishing): bump version | ||
|
|
||
| Update `version = "..."` in `pyproject.toml` and then repeat the build/upload steps. | ||
|
|
||
| --- | ||
|
|
||
| ## Install `nightops` in another project | ||
|
|
||
| ### Step 1: Create/activate a virtualenv | ||
| ```bash | ||
| python3 -m venv .venv | ||
| source .venv/bin/activate | ||
| ``` | ||
|
|
||
| ### Step 2: Install the published package | ||
| ```bash | ||
| pip install nightops | ||
| ``` | ||
|
|
||
| Or pin a version: | ||
| ```bash | ||
| pip install "nightops==0.1.0b1" | ||
| ``` | ||
|
|
||
| ### Step 2b (if using TestPyPI) | ||
| ```bash | ||
| pip install --index-url https://test.pypi.org/simple/ "nightops==0.1.0b1" | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Using `nightops` in the other project | ||
|
|
||
| The CLI expects a config file path via `--config` (or it will look for `config/nightops.yaml` relative to your current working directory). | ||
|
|
||
| ### Step 1: Copy/create config in the other repo | ||
| Create something like: | ||
| ```text | ||
| other-repo/ | ||
| config/ | ||
| nightops.yaml | ||
| ``` | ||
|
|
||
| ### Step 2: Run | ||
| From the other project’s root (adjust paths if your layout differs): | ||
|
|
||
| ```bash | ||
| nightops verify --config ./config/nightops.yaml | ||
|
|
||
| nightops agent run --simple \ | ||
| --incident "pod OOMKilled" \ | ||
| --config ./config/nightops.yaml | ||
| ``` | ||
|
|
||
| Or pass an explicit path (works from any working directory): | ||
|
|
||
| ```bash | ||
| OTHER_REPO="/path/to/your/other/project" # set to your checkout | ||
| nightops verify --config "$OTHER_REPO/config/nightops.yaml" | ||
| ``` | ||
|
|
||
| ### Step 3: Optional: run watch mode | ||
| ```bash | ||
| nightops agent watch --simple \ | ||
| --config ./config/nightops.yaml | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Important note about config files | ||
|
|
||
| `nightops` ships default YAML files inside the installed package, so the CLI can start even if the consuming repo doesn’t provide `config/nightops.yaml`. | ||
|
|
||
| In practice, you will still want to provide your own `config/nightops.yaml` in the consuming repo (so you can set your real GCP/Grafana/Slack values) and pass `--config` to override defaults. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../ |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,15 +2,55 @@ | |||||||||||
|
|
||||||||||||
| from __future__ import annotations | ||||||||||||
|
|
||||||||||||
| import logging | ||||||||||||
| import os | ||||||||||||
| import re | ||||||||||||
| from pathlib import Path | ||||||||||||
| from typing import Literal, Optional | ||||||||||||
| from typing import Any, Literal, Optional | ||||||||||||
|
|
||||||||||||
| from importlib.resources import files as pkg_files | ||||||||||||
|
|
||||||||||||
| import yaml | ||||||||||||
| from pydantic import Field, field_validator | ||||||||||||
| from pydantic import Field, ValidationError, field_validator | ||||||||||||
| from pydantic_settings import BaseSettings | ||||||||||||
|
|
||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _coerce_yaml_root(data: object) -> dict[str, Any]: | ||||||||||||
| """Normalize yaml.safe_load output: None/empty document -> {}, require a mapping.""" | ||||||||||||
| if data is None: | ||||||||||||
| return {} | ||||||||||||
| if not isinstance(data, dict): | ||||||||||||
| raise ValueError(f"YAML root must be a mapping, got {type(data).__name__}") | ||||||||||||
| return data | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _merge_mcp_servers_block(data: dict[str, Any]) -> None: | ||||||||||||
| """Inline `mcp_servers:` into flat fields; map cloud_logging -> cloud_logging_custom.""" | ||||||||||||
| if "mcp_servers" not in data: | ||||||||||||
| return | ||||||||||||
| mcp = data.pop("mcp_servers") | ||||||||||||
| if not isinstance(mcp, dict): | ||||||||||||
| raise ValueError("mcp_servers must be a mapping") | ||||||||||||
| if "cloud_logging" in mcp: | ||||||||||||
| mcp["cloud_logging_custom"] = mcp.pop("cloud_logging") | ||||||||||||
| data.update(mcp) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _parse_yaml_config_text(raw: str) -> dict[str, Any]: | ||||||||||||
| """Substitute `${VAR}` from the environment, parse YAML, normalize `mcp_servers`. | ||||||||||||
|
|
||||||||||||
| Shared by `from_yaml` (after optional `config/.env` load) and `from_yaml_text` | ||||||||||||
| (packaged defaults with no adjacent `.env`). | ||||||||||||
| """ | ||||||||||||
| for key, value in os.environ.items(): | ||||||||||||
| raw = raw.replace(f"${{{key}}}", value) | ||||||||||||
| raw = re.sub(r"\$\{[A-Za-z_][A-Za-z0-9_]*\}", "", raw) | ||||||||||||
|
Comment on lines
+47
to
+49
|
||||||||||||
| for key, value in os.environ.items(): | |
| raw = raw.replace(f"${{{key}}}", value) | |
| raw = re.sub(r"\$\{[A-Za-z_][A-Za-z0-9_]*\}", "", raw) | |
| pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}") | |
| raw = pattern.sub(lambda m: os.environ.get(m.group(1), ""), raw) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,8 @@ | |||||||||||||||
| from pathlib import Path | ||||||||||||||||
| from typing import Any, Optional | ||||||||||||||||
|
|
||||||||||||||||
| from importlib.resources import files as pkg_files | ||||||||||||||||
|
|
||||||||||||||||
| import yaml | ||||||||||||||||
|
|
||||||||||||||||
| from nightops.core.models import RemediationAction, RemediationPolicy, Severity | ||||||||||||||||
|
|
@@ -140,6 +142,47 @@ def _load_policies(self, path: str) -> None: | |||||||||||||||
| """Load policies from a YAML file, merging with defaults.""" | ||||||||||||||||
| policy_file = Path(path) | ||||||||||||||||
| if not policy_file.exists(): | ||||||||||||||||
|
||||||||||||||||
| if not policy_file.exists(): | |
| if not policy_file.exists(): | |
| # Only attempt packaged lookup for non-absolute paths to avoid | |
| # ambiguities when users pass absolute filesystem locations. | |
| if policy_file.is_absolute(): | |
| logger.info("No policy file at %s, using defaults", path) | |
| return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The runtime code now depends on packaged resources (
nightops/config/nightops.yaml). This change force-includesconfig/for wheels, but it may still be missing from sdists depending on hatch configuration, which can break installs that build from sdist. Consider also includingconfig/in the sdist target (or otherwise ensuring all package builds include these YAML resources).