Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vale/styles/spelling-exceptions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ REST
resources
schema_mapping
sdk
stderr
stdout
subcommand
subnet
subtyping
Expand Down
1 change: 1 addition & 0 deletions changelog/952.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `infrahubctl marketplace get` for fetching schemas and collections from the Infrahub Marketplace. Auto-detects schemas vs collections by namespace/name, supports `--version` for pinning, `--collection` to force the collection path, `--stdout` to stream content for piping (status messages on stderr), and `--marketplace-url` / `INFRAHUB_MARKETPLACE_URL` to point at staging or local instances.
36 changes: 36 additions & 0 deletions dev/specs/001-marketplace-api-update/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Specification Quality Checklist: Marketplace Download Command Update

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-21
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- The spec intentionally references the REST API at a behavioural level only; specific endpoint paths appear only in the informational "Implementation Status" section, which is explicitly outside acceptance and describes what the current branch has already shipped.
- The "schema wins on collision" precedence is stated as an assumption rather than as a clarification-blocker because it mirrors the pre-existing behaviour; revisit during `/speckit.clarify` if stakeholders want a different default.
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`.
95 changes: 95 additions & 0 deletions dev/specs/001-marketplace-api-update/contracts/marketplace-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# External Contract: Marketplace REST API

**Scope**: This document reverse-describes the subset of the `marketplace.infrahub.app` REST API that the `infrahubctl marketplace download` command relies on. **This is not a contract we own** — it captures the CLI's assumptions so that drift between client expectation and server reality can be caught in tests.

**Base URL**: `https://marketplace.infrahub.app` (default). Overridable via `--marketplace-url` or the `marketplace_url` setting in `infrahubctl.toml` / `INFRAHUB_MARKETPLACE_URL` env.

---

## Endpoint 1 — Download latest schema

```text
GET {base_url}/api/v1/schemas/{namespace}/{name}/download
```

**Success (200)**:

- `Content-Type`: `application/yaml` (or `text/yaml` / `text/plain`)
- Response body: raw YAML payload of the schema.
- Response header: `x-schema-version: <semver>` — resolved version the server returned. Required for the CLI to echo the version to the user.

**Not found (404)**: JSON body `{"detail": "<reason>"}`. Treated by the CLI as input to the not-found / auto-detect fallback flow.

**Other 4xx**: JSON body `{"detail": "<reason>"}`; surfaced as an error with the detail message.

**5xx / transport failure**: surfaced as a network-class error (see `research.md` R-4).

---

## Endpoint 2 — Download specific schema version

```text
GET {base_url}/api/v1/schemas/{namespace}/{name}/versions/{version}/download
```

Same response contract as Endpoint 1, but `{version}` is a user-supplied semver.

**404 semantics**:

- If the schema itself does not exist, a 404 is returned. Cannot be distinguished from "version missing" by URL alone, so the CLI MUST probe the unversioned endpoint first when constructing a "version-not-found" vs. "not-found" error (see `research.md` R-4).
- If the schema exists but the specific version is not published, a 404 is returned.

---

## Endpoint 3 — Download collection

```text
GET {base_url}/api/v1/collections/{namespace}/{name}/download
```

**Success (200)**: JSON body with shape:

```json
{
"collection": {
"namespace": "acme",
"name": "starter-pack",
"schema_count": 2,
"downloaded_count": 2,
"skipped": [
{ "namespace": "acme", "name": "broken", "reason": "no published version" }
]
},
"schemas": [
{
"namespace": "acme",
"name": "network-base",
"semver": "1.0.0",
"filename": "acme-network-base-1.0.0.yml",
"content": "---\n..."
}
]
}
```

The CLI writes each `schemas[].content` to disk under `<output_dir>/<collection_name>/<schemas[].name>.yml` (today) or a flat layout under `<output_dir>/` (future — see implementation notes below).

**404 / other errors**: same as Endpoint 1.

---

## Implicit contract for auto-detection

The CLI issues Endpoints 1 and 3 in parallel when the user omits `--collection`. The contract assumed is:

- Both endpoints are idempotent and safe to issue concurrently.
- Either endpoint responds with a well-formed 404 (not a 5xx) when the identifier is not present as that item type. A 5xx from either probe is treated as a transport failure and aborts auto-detection.
- If both endpoints return 200, the CLI applies the documented "schema wins" precedence (`research.md` R-3).

**Follow-up (out of scope)**: Request the marketplace team add lightweight metadata endpoints (`GET /api/v1/schemas/{ns}/{name}` and `GET /api/v1/collections/{ns}/{name}` without `/download`) so the CLI can probe cheaply without paying for the payload on the wasted side of a 200-200 collision. When those ship, the CLI swaps its probe targets and keeps the download endpoints only for the winner.

---

## Versions of this contract

This document reflects the API shape observed on or before **2026-04-21**. Any new fields added server-side are expected to be backward compatible; breaking changes require a coordinated CLI update.
102 changes: 102 additions & 0 deletions dev/specs/001-marketplace-api-update/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Implementation Plan: Marketplace Download Command Update

**Branch**: `knotty-dibble` | **Date**: 2026-04-21 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `specs/001-marketplace-api-update/spec.md`

## Summary

Migrate `infrahubctl marketplace download` to the public REST API at `marketplace.infrahub.app`, add auto-detection of schema-vs-collection identifiers, keep the `--version` pinning flag for schemas, and retain `--output-dir` (default `./schemas`). The REST migration, `--version`, and `--output-dir` pieces have already shipped in the current branch's `Marketplace` commit; the remaining deltas are auto-detection, the four-class error taxonomy, and the documented precedence rule for namespace/name collisions.

## Technical Context

**Language/Version**: Python 3.10–3.13
**Primary Dependencies**: `typer` (CLI), `httpx` (HTTP), `rich` (console output), `pydantic` 2.x (config). No new runtime dependencies are expected.
**Storage**: Files on disk under the user-chosen `--output-dir` (default `./schemas`). No database or server-side state owned by this change.
**Testing**: `pytest` with `pytest-httpx` for mocking the marketplace REST API; existing tests in `tests/unit/ctl/test_marketplace_app.py` are the template.
**Target Platform**: Cross-platform CLI (macOS/Linux/Windows), installed via `uv`/`pip` as the `infrahubctl` entry point.
**Project Type**: Single project (Python SDK + CLI) — uses the `infrahub_sdk` package layout already in place.
**Performance Goals**: Interactive CLI; target end-to-end command latency dominated by the marketplace round-trip. Auto-detection must not exceed one additional round-trip beyond the download itself in the common (cache-miss, 404-on-schema) case.
**Constraints**: Must remain backward compatible with scripts that already pass `--collection` / `--output-dir` / `--version`. Must not require any new public SDK surface outside the CLI module. Must not re-introduce GraphQL usage for marketplace calls.
**Scale/Scope**: Small surface area — a single `infrahub_sdk/ctl/marketplace.py` module (~180 LOC today) plus its test file. Expected diff is additive, likely under ~150 LOC of new code plus tests.

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

This repository ships a template-only `.specify/memory/constitution.md` (unfilled) in sibling worktrees, and none in the current branch. In the absence of concrete articles, the following project-level gates (derived from `AGENTS.md`) are applied:

- **Async/sync dual pattern**: N/A for this change — the marketplace CLI command is already async-only (via `AsyncTyper`) and deliberately does not expose a sync twin. Adding a sync twin is out of scope.
- **Type hints on all signatures**: PASS — will be enforced on any new helpers.
- **No modifications to generated code (`protocols.py`)**: PASS — not touched.
- **No new runtime dependencies without asking first**: PASS — no new dependencies planned.
- **Lint + format gates (`uv run invoke format lint-code`)**: PASS — will be run before committing.
- **Tests-first spirit**: Will add unit tests covering each new acceptance scenario before wiring the CLI changes.

**Result**: PASS. No violations to justify.

## Project Structure

### Documentation (this feature)

```text
specs/001-marketplace-api-update/
├── spec.md # Feature specification (already written)
├── plan.md # This file
├── research.md # Phase 0 output
├── contracts/
│ └── marketplace-api.md # Reverse-documented external REST contract we consume
├── quickstart.md # Phase 1 output
├── checklists/
│ └── requirements.md # Spec quality validation (already written)
└── tasks.md # (Produced later by /speckit.tasks)
```

`data-model.md` is intentionally omitted: this feature consumes an external REST API and writes YAML to disk, with no owned entity model to design. The Key Entities in `spec.md` are behavioural, not a data-store schema.

### Source Code (repository root)

```text
infrahub_sdk/
├── ctl/
│ ├── marketplace.py # Primary code under change
│ ├── config.py # `marketplace_url` setting (already in place)
│ └── cli_commands.py # Registers the marketplace sub-app (no change expected)

tests/
└── unit/
└── ctl/
└── test_marketplace_app.py # Primary test file to extend
```

**Structure Decision**: Single-project Python layout. All implementation lives in the existing `infrahub_sdk/ctl/marketplace.py` module; tests live alongside existing tests in `tests/unit/ctl/test_marketplace_app.py`. No new packages or modules are introduced.

## Phases

### Phase 0 — Research

See [research.md](research.md). Topics resolved:

1. Auto-detection strategy (schema-probe-first vs. parallel probe vs. metadata endpoint).
2. Name collision precedence (`schema wins` — see Assumption in spec).
3. Error taxonomy mapping (not-found vs. version-not-found vs. network vs. invalid-input) onto observable HTTP responses from the marketplace.
4. Behaviour when a schema exists only as a pre-release (no stable published semver).

### Phase 1 — Design & Contracts

**Prerequisites:** `research.md` complete.

1. **External contract documentation** → [contracts/marketplace-api.md](contracts/marketplace-api.md).
Documents the *consumed* REST endpoints (schemas, collections, version pinning), expected response shapes, and HTTP status semantics. This is not an owned contract — it reverse-describes what the CLI assumes about `marketplace.infrahub.app`, so drift can be detected.

2. **Quickstart** → [quickstart.md](quickstart.md).
Manual and automated verification steps that exercise each acceptance scenario from the spec.

3. **Agent context update**: not applicable — no `.specify/scripts/bash/update-agent-context.sh` is installed in this branch, and the project-level agent context (`AGENTS.md`) does not require updates for this feature (no new dependencies or commands).

### Phase 2 — Tasks (deferred)

Not produced by this command. Will be generated by `/speckit.tasks` against this plan.

## Complexity Tracking

No constitution violations to justify. The feature fits inside the existing module with no new abstractions.
103 changes: 103 additions & 0 deletions dev/specs/001-marketplace-api-update/quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Quickstart: Marketplace Download Command

End-to-end smoke test for the updated `infrahubctl marketplace download` command. These steps exercise each acceptance scenario in `spec.md` and should be green before merging.

## Prerequisites

```bash
uv sync --all-groups --all-extras
```

## Unit tests (fast loop)

```bash
uv run pytest tests/unit/ctl/test_marketplace_app.py -v
```

All existing tests must stay green, and new tests MUST be added to cover:

- Auto-detection when identifier is a schema (no `--collection` passed).
- Auto-detection when identifier is a collection (no `--collection` passed).
- Auto-detection when both endpoints return 200 — resolved as schema, type printed in output.
- Auto-detection when both endpoints return 404 — error class "not found".
- `--version` with an unpublished version — error class "version not found".
- 5xx on either probe endpoint — error class "network".
- Invalid identifier (no slash) — error class "invalid input" — caught before any network call.

## Manual verification against the public marketplace

```bash
# Scenario 1: download a schema by auto-detection
uv run infrahubctl marketplace download acme/network-base
ls schemas/

# Scenario 2: download a collection by auto-detection
uv run infrahubctl marketplace download acme/starter-pack
ls schemas/

# Scenario 3: pin a specific schema version
uv run infrahubctl marketplace download acme/network-base --version 0.9.0
grep '^version:' schemas/network-base.yml

# Scenario 4: custom output directory
uv run infrahubctl marketplace download acme/network-base --output-dir ./tmp/market-test
ls ./tmp/market-test

# Scenario 5: explicit --collection still works (override path)
uv run infrahubctl marketplace download acme/starter-pack --collection

# Scenario 6: version on a collection emits a warning, proceeds
uv run infrahubctl marketplace download acme/starter-pack --version 1.0.0
# Expect: "Warning: --version is ignored when downloading a collection." followed by success output.
```

## Manual verification against a local/staging marketplace

```bash
uv run infrahubctl marketplace download acme/test \
--marketplace-url http://localhost:8000 \
--output-dir ./tmp/local-market
```

This must exercise the same auto-detection behaviour against the overridden host.

## Expected success output shape

For a schema:

```text
Downloaded schema acme/network-base v1.2.0 -> schemas/network-base.yml
```

For a collection:

```text
Downloaded acme/network-base v1.0.0 -> schemas/starter-pack/network-base.yml
Downloaded acme/dcim v2.1.0 -> schemas/starter-pack/dcim.yml

Collection acme/starter-pack: 2/2 schemas downloaded
```

The CLI MUST announce the resolved item type (schema vs. collection) explicitly so the user can detect an unintended match in a collision case.

## Expected error output shapes

```text
# Not found
No schema or collection named 'acme/missing' found on marketplace.infrahub.app

# Version not found
Schema 'acme/network-base' has no published version '9.9.9'. Run without --version for the latest.

# Network
Could not reach marketplace at https://marketplace.infrahub.app: connection timed out. Check your connection or --marketplace-url.

# Invalid input
Invalid identifier 'acme-network-base'. Expected format: namespace/name
```

## Lint / format / type gates (must all pass)

```bash
uv run invoke format lint-code
```
Loading