Skip to content

Commit 94a0c49

Browse files
authored
af: layered config (global / project-shared / project-local) (#207)
1 parent 7cbaab4 commit 94a0c49

16 files changed

Lines changed: 4115 additions & 2029 deletions

File tree

astro-airflow-mcp/README.md

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@
1515
- [Core Tools](#core-tools)
1616
- [MCP Resources](#mcp-resources)
1717
- [MCP Prompts](#mcp-prompts)
18-
- [Airflow CLI Tool](#af-tool)
18+
- [Airflow CLI Tool](#airflow-cli-tool)
19+
- [Installation](#installation)
20+
- [Quick Reference](#quick-reference)
1921
- [Instance Management](#instance-management)
22+
- [Configuration scopes](#configuration-scopes)
2023
- [Instance Discovery](#instance-discovery)
24+
- [Direct API Access](#direct-api-access)
25+
- [Configuration](#configuration-1)
26+
- [Registry Caching](#registry-caching)
2127
- [Advanced Usage](#advanced-usage)
2228
- [Running as Standalone Server](#running-as-standalone-server)
2329
- [Airflow Plugin Mode](#airflow-plugin-mode)
2430
- [CLI Options](#cli-options)
31+
- [Telemetry](#telemetry)
2532
- [Architecture](#architecture)
2633
- [Core Components](#core-components)
2734
- [Version Handling Strategy](#version-handling-strategy)
@@ -343,13 +350,60 @@ af instance add corp --url https://airflow.corp.example.com --no-verify-ssl --us
343350
af instance add corp --url https://airflow.corp.example.com --ca-cert /path/to/ca-bundle.pem --token '${TOKEN}'
344351

345352
# List and switch instances
346-
af instance list # Shows all instances in a table
347-
af instance use prod # Switch to prod instance
348-
af instance current # Show current instance
353+
af instance list # All instances, with scope and current marker
354+
af instance use prod # Switch to prod
355+
af instance current # Show current instance
356+
af instance show prod # Show instance details + which file it came from
349357
af instance delete old-instance
350-
af instance reset # Reset to default configuration
358+
af instance reset # Reset global config to default
351359
```
352360

361+
### Configuration scopes
362+
363+
`af` reads and writes config across three scopes, co-located with the Astro CLI's `~/.astro/` directory and any project's `.astro/` directory. The model mirrors `git config` (system / global / local).
364+
365+
| Scope | File | Committed? | Use for |
366+
|---|---|---|---|
367+
| Global | `~/.astro/config.yaml` | n/a (per-user) | Personal default deployments, localhost |
368+
| Project shared | `<root>/.astro/config.yaml` | yes (recommended) | The team's deployment inventory for this project |
369+
| Project local | `<root>/.astro/config.local.yaml` | no (gitignored) | Personal `current-instance` and per-developer overrides |
370+
371+
`<root>` is found by walking up from `cwd` looking for a `.astro/` directory. Once you're in a project, `af instance discover` populates the team's deployment list directly into the committed file, so a fresh clone + `astro login` gets every teammate working immediately.
372+
373+
**Read precedence (most-specific wins):** `current-instance` from project-local, then global. Instance lookup: project-local → project-shared → global; same-named entries in narrower scopes shadow.
374+
375+
**Write routing** (default, no flags):
376+
- `add`: project-shared inside a project, else global
377+
- `use`: project-local (each developer can target a different deployment without touching the committed file)
378+
- `delete`: most-specific scope that has the name (rerun to peel scopes)
379+
- `discover`: project-shared inside a project, else global
380+
381+
Override with mutually-exclusive flags on `add` / `use` / `delete` / `discover`:
382+
383+
```bash
384+
af instance add local-dev --url http://localhost:8080 --global
385+
af instance use prod --local # implicit, but explicit works
386+
af instance delete prod --global # only delete the global copy
387+
af instance discover --project # populate the team inventory
388+
```
389+
390+
**Project-shared is committed by default**, so prefer `astro_pat` (which stores no secret on disk) or `${ENV_VAR}` interpolation when adding token/basic auth there. For literal credentials you don't want in git, use `--local` (gitignored) or `--global`. `af` does not police what you put where — gitignore is the contract.
391+
392+
Use `AF_CONFIG=<path>` (or `--config <path>`) to bypass layering entirely and read/write a single file — preserves the `astro otto` wrapper's `AF_CONFIG=/dev/null` neutralize-config sentinel.
393+
394+
#### Migrating from `~/.af/config.yaml`
395+
396+
Older versions of `af` stored config at `~/.af/config.yaml`. On first read, that file is honored as a one-time fallback (with a stderr deprecation note). Run `af migrate` to move it explicitly:
397+
398+
```bash
399+
af migrate
400+
# {"status": "migrated", "from": "/Users/julian/.af/config.yaml",
401+
# "to": "/Users/julian/.astro/config.yaml",
402+
# "backup": "/Users/julian/.af/config.yaml.bak"}
403+
```
404+
405+
The migration preserves any astro-cli content already in `~/.astro/config.yaml` (the `save` merge keeps unknown top-level keys like `project`, `cloud`, `contexts`). Re-runs are idempotent.
406+
353407
### Instance Discovery
354408

355409
Auto-discover Airflow instances from Astro Cloud or local Docker environments:
@@ -372,11 +426,13 @@ af instance discover local
372426

373427
# Deep scan all ports for local instances
374428
af instance discover local --scan
375-
```
376429

377-
> **Note:** Always run with `--dry-run` first. The Astro discovery backend creates API tokens in Astro Cloud, so review the list before confirming. Token names are user-specific (for example, `af-discover-<user>`) to avoid collisions when multiple users discover the same deployment.
430+
# Force a specific scope
431+
af instance discover --global # personal global config, even when in a project
432+
af instance discover astro --project # explicit (this is also the default in a project)
433+
```
378434

379-
Config file location: `~/.af/config.yaml` (override with `--config` or `AF_CONFIG` env var)
435+
Discovered Astro instances use `astro_pat` auth — they store only the deployment ID and the active `astro` context, not a token. Tokens are resolved from the user's `astro login` session at request time, so `.astro/config.yaml` is safe to commit.
380436

381437
### Direct API Access
382438

@@ -415,6 +471,10 @@ af api spec
415471
- `-f key=value`: Keeps value as raw string
416472
- `--body '{}'`: Raw JSON body for complex objects
417473

474+
#### Config file format
475+
476+
Each scope file (global / project-shared / project-local) shares the same schema. `af` rewrites only its own top-level keys (`instances`, `current-instance`); other keys (e.g. astro-cli's `project`, `cloud`, `contexts`) are preserved untouched on round-trip. Example:
477+
418478
```yaml
419479
instances:
420480
- name: local
@@ -428,7 +488,13 @@ instances:
428488
- name: prod
429489
url: https://prod.example.com
430490
auth:
431-
token: ${AIRFLOW_PROD_TOKEN} # Environment variable interpolation
491+
token: ${AIRFLOW_PROD_TOKEN} # Env-var interpolation; safe to commit
492+
- name: prod-astro
493+
url: https://prod.astronomer.run/dXYZ
494+
auth:
495+
kind: astro_pat # Resolves from `astro login` at request time
496+
context: astronomer.io
497+
deployment_id: clXYZ123 # Diagnostic only; tokens are not stored
432498
- name: corporate
433499
url: https://airflow.corp.example.com
434500
auth:
@@ -439,6 +505,8 @@ instances:
439505
current-instance: local
440506
```
441507
508+
Project-shared (`.astro/config.yaml`) is committed by default, so prefer `astro_pat` or `${VAR}` interpolation for credentials there. Put literal secrets in project-local (`.astro/config.local.yaml`, gitignored) instead.
509+
442510
### Configuration
443511

444512
Configure connections via environment variables:
@@ -600,7 +668,7 @@ For open-source Airflow, the plugin inherits Airflow's native RBAC. A user with
600668

601669
| Flag | Environment Variable | Description |
602670
|------|---------------------|-------------|
603-
| `--config`, `-c` | `AF_CONFIG` | Path to config file (default: `~/.af/config.yaml`) |
671+
| `--config`, `-c` | `AF_CONFIG` | Single-file mode: bypass layered config and use this file as the entire config (default layering reads `~/.astro/config.yaml` and any project `.astro/config.{yaml,local.yaml}` it walks up to) |
604672
| `--version`, `-v` | | Show version and exit |
605673

606674
### Telemetry

astro-airflow-mcp/src/astro_airflow_mcp/cli/context.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,20 @@ def get_instance(cls) -> CLIContext:
3535
return cls._instance
3636

3737
def _load_from_config(self) -> ResolvedConfig | None:
38-
"""Load configuration from config file.
38+
"""Load configuration via the layered config (global + project).
39+
40+
Read precedence: project-local > project-shared > global. When
41+
``AF_CONFIG`` is set, layering is skipped and the single file is
42+
used (preserves the ``astro otto`` AF_CONFIG=/dev/null wrapper
43+
semantics).
3944
4045
Returns:
4146
ResolvedConfig if available, None otherwise
4247
"""
4348
try:
44-
from astro_airflow_mcp.config import ConfigError, ConfigManager
49+
from astro_airflow_mcp.config import ConfigError, LayeredConfig
4550

46-
manager = ConfigManager()
47-
return manager.resolve_instance()
51+
return LayeredConfig().resolve_instance()
4852
except FileNotFoundError:
4953
# No config file - this is normal for first-time users
5054
return None

0 commit comments

Comments
 (0)