docs(arch): resolve OQ-050 — include docker/system/events in v1
Resolve the deferred docker system events subscription question. docker/system/events is now a v1 Subscription operation using the same StreamingHandler pattern already wired for logs, exec, and image/pull. The internal ownership-store subscription for stale-entry cleanup on destroy events is a follow-up refinement. Scrub hedging language from ADR-060 and docker specs: - Remove 'marginal gain' / 'future feature is additive' framing - Replace 'no reaper' / 'not promptly cleaned up' with clean statement that events subscription provides the prompt-cleanup path - Remove stale-entry policy from ADR-060's two-way door classification
This commit is contained in:
1 parent
11f531131c
commit
ab963d0e46
6 files changed
+108
-64
No files matched your search
@@ -17,14 +17,14 @@ sessions into containers.
|
|||||||
alknet-docker does two things:
|
alknet-docker does two things:
|
||||||
|
|
||||||
1. **Call operations.** A set of `OperationSpec`-registered operations
|
1. **Call operations.** A set of `OperationSpec`-registered operations
|
||||||
(`docker/container/*`, `docker/image/*`) on the shared
|
(`docker/container/*`, `docker/image/*`, `docker/system/events`) on
|
||||||
`alknet/call` ALPN, mapping bollard's docker API to the call
|
the shared `alknet/call` ALPN, mapping bollard's docker API to the
|
||||||
protocol's `Query` / `Mutation` / `Subscription` dispatch paths.
|
call protocol's `Query` / `Mutation` / `Subscription` dispatch paths.
|
||||||
Lifecycle (create/start/stop/remove/list/inspect) is `Query`/
|
Lifecycle (create/start/stop/remove/list/inspect) is `Query`/
|
||||||
`Mutation`; logs, non-interactive exec, and image pull are
|
`Mutation`; logs, non-interactive exec, image pull, and system
|
||||||
`Subscription` (streaming via `StreamingHandler`, ADR-049). The
|
events are `Subscription` (streaming via `StreamingHandler`,
|
||||||
operations declare `AccessControl` against the ADR-050 container-
|
ADR-049). The operations declare `AccessControl` against the
|
||||||
as-resource model. Decided in
|
ADR-050 container-as-resource model. Decided in
|
||||||
[ADR-058](../../decisions/058-alknet-docker-on-alknet-call.md).
|
[ADR-058](../../decisions/058-alknet-docker-on-alknet-call.md).
|
||||||
|
|
||||||
2. **`DockerTtyBackend`** (behind the `tty` feature). An
|
2. **`DockerTtyBackend`** (behind the `tty` feature). An
|
||||||
@@ -90,7 +90,7 @@ docs):
|
|||||||
|----|-------|--------|-----------|
|
|----|-------|--------|-----------|
|
||||||
| OQ-048 | Network and volume operation surface | deferred(scope) | Network/volume CRUD deferred; v1 is containers + images |
|
| OQ-048 | Network and volume operation surface | deferred(scope) | Network/volume CRUD deferred; v1 is containers + images |
|
||||||
| OQ-049 | Image build (buildkit) scope | deferred(scope) | `buildkit` feature deferred; v1 has `image/pull` + `image/list` + `image/inspect` |
|
| OQ-049 | Image build (buildkit) scope | deferred(scope) | `buildkit` feature deferred; v1 has `image/pull` + `image/list` + `image/inspect` |
|
||||||
| OQ-050 | Docker system events subscription | deferred(scope) | `docker/system/events` subscription for stale-ownership cleanup deferred |
|
| OQ-050 | Docker system events subscription | resolved | `docker/system/events` included in v1 as a `Subscription` operation; internal ownership-store subscription for cleanup is a follow-up refinement |
|
||||||
| OQ-051 | Container create options surface | deferred(scope) | Full `CreateContainerOptions` (mounts, port bindings, networks) surface deferred to v1 implementation |
|
| OQ-051 | Container create options surface | deferred(scope) | Full `CreateContainerOptions` (mounts, port bindings, networks) surface deferred to v1 implementation |
|
||||||
|
|
||||||
## Key Design Principles
|
## Key Design Principles
|
||||||
|
|||||||
@@ -67,13 +67,13 @@ applied.
|
|||||||
| `docker/image/list` | Query | `list_images` | (none) | |
|
| `docker/image/list` | Query | `list_images` | (none) | |
|
||||||
| `docker/image/pull` | Subscription | `create_image` | (none) | StreamingHandler; progress events |
|
| `docker/image/pull` | Subscription | `create_image` | (none) | StreamingHandler; progress events |
|
||||||
| `docker/image/inspect` | Query | `inspect_image` | (none) | |
|
| `docker/image/inspect` | Query | `inspect_image` | (none) | |
|
||||||
|
| `docker/system/events` | Subscription | `events` | (none) | StreamingHandler; daemon events (container start/stop/die/destroy, image pull/tag/delete, etc.) |
|
||||||
|
|
||||||
**Out of scope for v1 (deferred):**
|
**Out of scope for v1 (deferred):**
|
||||||
|
|
||||||
- Network operations (`docker/network/*`) — OQ-048.
|
- Network operations (`docker/network/*`) — OQ-048.
|
||||||
- Volume operations (`docker/volume/*`) — OQ-048.
|
- Volume operations (`docker/volume/*`) — OQ-048.
|
||||||
- Image build (`docker/image/build` with buildkit) — OQ-049.
|
- Image build (`docker/image/build` with buildkit) — OQ-049.
|
||||||
- System events (`docker/system/events`) — OQ-050.
|
|
||||||
- Full `CreateContainerOptions` surface (mounts, port bindings,
|
- Full `CreateContainerOptions` surface (mounts, port bindings,
|
||||||
networks) — OQ-051. v1 `create` accepts the image, command, env,
|
networks) — OQ-051. v1 `create` accepts the image, command, env,
|
||||||
labels, and name; the full options surface is a v1 implementation
|
labels, and name; the full options surface is a v1 implementation
|
||||||
@@ -181,9 +181,11 @@ overwritten by the handler to prevent a caller spoofing ownership).
|
|||||||
3. Returns `call.responded` with an empty/ok result.
|
3. Returns `call.responded` with an empty/ok result.
|
||||||
|
|
||||||
Autonomous container death (a `--rm` exit, external `docker rm`,
|
Autonomous container death (a `--rm` exit, external `docker rm`,
|
||||||
daemon restart) is tolerated — stale ownership entries are not
|
daemon restart) is tolerated — stale ownership entries are inert
|
||||||
promptly cleaned up (no reaper); they're inert (a reused container ID
|
(a reused container ID gets a fresh `record` on its next `create`).
|
||||||
gets a fresh `record` on its next `create`). See ADR-060 §4.
|
The `docker/system/events` subscription provides the prompt-cleanup
|
||||||
|
path (internal ownership-store subscription on `destroy` events).
|
||||||
|
See ADR-060 §4.
|
||||||
|
|
||||||
### `docker/container/list` — scope-gate + optional result-filter
|
### `docker/container/list` — scope-gate + optional result-filter
|
||||||
|
|
||||||
@@ -317,6 +319,44 @@ each progress event → `call.responded`, stream end → `call.completed`.
|
|||||||
No exit code (image pull has no exit code; success is stream end
|
No exit code (image pull has no exit code; success is stream end
|
||||||
without error).
|
without error).
|
||||||
|
|
||||||
|
#### `docker/system/events`
|
||||||
|
|
||||||
|
Maps `bollard::system::events()` (returns
|
||||||
|
`Stream<Item = Result<SystemEventsMessage, Error>>`) to a stream of
|
||||||
|
`call.responded` frames. Same shape as logs: each daemon event
|
||||||
|
(container start/stop/die/destroy, image pull/tag/delete, network
|
||||||
|
create/connect, volume create/mount, etc.) → `call.responded`, stream
|
||||||
|
end on client disconnect or daemon connection close →
|
||||||
|
`call.completed`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let docker_clone = docker.clone();
|
||||||
|
let handler: StreamingHandler = Arc::new(move |_input, _ctx| {
|
||||||
|
let docker = docker_clone.clone();
|
||||||
|
Box::pin(async_stream::stream! {
|
||||||
|
let mut stream = docker.events(None::<EventsOptions<String>>);
|
||||||
|
while let Some(result) = stream.next().await {
|
||||||
|
match result {
|
||||||
|
Ok(event) => yield ResponseEnvelope::ok(serde_json::to_value(event)
|
||||||
|
.unwrap_or_default()),
|
||||||
|
Err(e) => yield ResponseEnvelope::error(CallError {
|
||||||
|
code: "DOCKER_ERROR".into(),
|
||||||
|
message: e.to_string(),
|
||||||
|
retryable: false, details: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as ResponseStream
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The operation has no `resource_id_path` and no `resource_type` — it
|
||||||
|
surfaces daemon-wide events, not per-container events. The scope
|
||||||
|
check (`system:events`) gates the call. The internal ownership-store
|
||||||
|
subscription for stale-entry cleanup on `destroy` events is a
|
||||||
|
follow-up refinement — the operation itself is the architecture
|
||||||
|
decision.
|
||||||
|
|
||||||
### Access Control
|
### Access Control
|
||||||
|
|
||||||
The operations declare `AccessControl` against the ADR-050 model,
|
The operations declare `AccessControl` against the ADR-050 model,
|
||||||
@@ -407,6 +447,24 @@ Images are not runtime-spawned resources with per-caller ownership
|
|||||||
— they're shared daemon state. The scope check gates; no ownership
|
— they're shared daemon state. The scope check gates; no ownership
|
||||||
provider consultation.
|
provider consultation.
|
||||||
|
|
||||||
|
#### System events (`system/events`)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
OperationSpec {
|
||||||
|
name: "docker/system/events",
|
||||||
|
access_control: AccessControl {
|
||||||
|
required_scopes: vec!["system:events".into()],
|
||||||
|
// no resource_type — daemon-wide events, not per-resource
|
||||||
|
..
|
||||||
|
},
|
||||||
|
// no resource_id_path
|
||||||
|
..
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Scope-gate only. The operation surfaces daemon-wide events; there is
|
||||||
|
no per-resource ownership to check.
|
||||||
|
|
||||||
### Error Schemas (ADR-023)
|
### Error Schemas (ADR-023)
|
||||||
|
|
||||||
Each operation declares its domain error codes in
|
Each operation declares its domain error codes in
|
||||||
@@ -475,10 +533,11 @@ connection, not a call operation). This is the same boundary as
|
|||||||
existing container whose ownership was recorded at create time
|
existing container whose ownership was recorded at create time
|
||||||
(or which pre-exists and is reached via the static fallback).
|
(or which pre-exists and is reached via the static fallback).
|
||||||
- **Stale ownership entries are tolerated.** Autonomous container
|
- **Stale ownership entries are tolerated.** Autonomous container
|
||||||
death leaves a stale entry; no reaper cleans it promptly. The
|
death leaves a stale entry; the `docker/system/events` subscription
|
||||||
entry is inert (a reused container ID gets a fresh `record`). A
|
provides the prompt-cleanup path (internal ownership-store
|
||||||
`docker/system/events` subscription for prompt cleanup is deferred
|
subscription on `destroy` events). Until that internal subscription
|
||||||
(OQ-050).
|
is wired, stale entries are inert (a reused container ID gets a
|
||||||
|
fresh `record`).
|
||||||
- **`owned_only` on `list` is the caller's choice.** Default
|
- **`owned_only` on `list` is the caller's choice.** Default
|
||||||
`false` (returns all); `true` filters to the caller's containers.
|
`false` (returns all); `true` filters to the caller's containers.
|
||||||
The default and the two cases are decided in ADR-060 §2; the scope
|
The default and the two cases are decided in ADR-060 §2; the scope
|
||||||
@@ -505,7 +564,6 @@ See [open-questions.md](../../open-questions.md) for full details.
|
|||||||
|
|
||||||
- **OQ-048** (deferred(scope)): Network and volume operation surface.
|
- **OQ-048** (deferred(scope)): Network and volume operation surface.
|
||||||
- **OQ-049** (deferred(scope)): Image build (buildkit) scope.
|
- **OQ-049** (deferred(scope)): Image build (buildkit) scope.
|
||||||
- **OQ-050** (deferred(scope)): Docker system events subscription.
|
|
||||||
- **OQ-051** (deferred(scope)): Container create options surface.
|
- **OQ-051** (deferred(scope)): Container create options surface.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ container?" branch.
|
|||||||
| `docker/image/list` | Query | `list_images` | JSON |
|
| `docker/image/list` | Query | `list_images` | JSON |
|
||||||
| `docker/image/pull` | Subscription | `create_image` | JSON (StreamingHandler) |
|
| `docker/image/pull` | Subscription | `create_image` | JSON (StreamingHandler) |
|
||||||
| `docker/image/inspect` | Query | `inspect_image` | JSON |
|
| `docker/image/inspect` | Query | `inspect_image` | JSON |
|
||||||
|
| `docker/system/events` | Subscription | `events` | JSON (StreamingHandler) |
|
||||||
|
|
||||||
Interactive exec (`tty: true`) and interactive attach are **not** call
|
Interactive exec (`tty: true`) and interactive attach are **not** call
|
||||||
operations — they are `alknet/tty` sessions via `DockerTtyBackend`.
|
operations — they are `alknet/tty` sessions via `DockerTtyBackend`.
|
||||||
@@ -318,7 +319,6 @@ See [open-questions.md](../../open-questions.md) for full details.
|
|||||||
|
|
||||||
- **OQ-048** (deferred(scope)): Network and volume operation surface.
|
- **OQ-048** (deferred(scope)): Network and volume operation surface.
|
||||||
- **OQ-049** (deferred(scope)): Image build (buildkit) scope.
|
- **OQ-049** (deferred(scope)): Image build (buildkit) scope.
|
||||||
- **OQ-050** (deferred(scope)): Docker system events subscription.
|
|
||||||
- **OQ-051** (deferred(scope)): Container create options surface.
|
- **OQ-051** (deferred(scope)): Container create options surface.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|||||||
@@ -234,16 +234,12 @@ alknet-managed remove path, with autonomous-death tolerance**:
|
|||||||
The tolerance for stale entries is intentional: ownership is runtime
|
The tolerance for stale entries is intentional: ownership is runtime
|
||||||
state (ADR-050 assumption 4), meaningless across restarts. The
|
state (ADR-050 assumption 4), meaningless across restarts. The
|
||||||
in-memory default store loses all entries on restart; a persistence
|
in-memory default store loses all entries on restart; a persistence
|
||||||
adapter would cache and invalidate. A reaper that subscribes to docker
|
adapter would cache and invalidate. The `docker/system/events`
|
||||||
daemon death events would be more prompt but adds a subscription
|
subscription operation provides the prompt-cleanup path: the
|
||||||
surface for a marginal gain (the stale entry is inert — it can't grant
|
ownership store subscribes internally to `destroy` events and revokes
|
||||||
access to a container that doesn't exist, and a reused container ID
|
stale entries. Until that internal subscription is wired, stale
|
||||||
gets a fresh `record` on its next `create`).
|
entries are inert (a reused container ID gets a fresh `record` on
|
||||||
|
its next `create`).
|
||||||
A future "prompt stale-entry cleanup" feature is additive: a
|
|
||||||
`docker/system/events` subscription operation (deferred, OQ-050) that
|
|
||||||
the ownership store could subscribe to. The base model tolerates stale
|
|
||||||
entries; the prompt-cleanup path is a refinement.
|
|
||||||
|
|
||||||
### 5. `docker/container/create` records ownership; `start`/`stop`/`restart` do not
|
### 5. `docker/container/create` records ownership; `start`/`stop`/`restart` do not
|
||||||
|
|
||||||
@@ -271,8 +267,8 @@ event; the lifecycle operations are management of an existing resource.
|
|||||||
`managed` flag marks alknet-spawned containers; the `owner` label
|
`managed` flag marks alknet-spawned containers; the `owner` label
|
||||||
carries the peer id for the `list` filter and the cross-check.
|
carries the peer id for the `list` filter and the cross-check.
|
||||||
- Teardown is handler-driven on the alknet path and tolerant of
|
- Teardown is handler-driven on the alknet path and tolerant of
|
||||||
autonomous death on the non-alknet path. No reaper subscription
|
autonomous death on the non-alknet path. The `docker/system/events`
|
||||||
surface in the base model.
|
subscription provides the prompt-cleanup path for stale entries.
|
||||||
- ADR-050's model is applied without amendment. The `list` case
|
- ADR-050's model is applied without amendment. The `list` case
|
||||||
(specific #4a), the teardown coupling (#4b), and the
|
(specific #4a), the teardown coupling (#4b), and the
|
||||||
backward-compat static-resource fallback (#2) all map cleanly to
|
backward-compat static-resource fallback (#2) all map cleanly to
|
||||||
@@ -308,13 +304,14 @@ event; the lifecycle operations are management of an existing resource.
|
|||||||
## Door type
|
## Door type
|
||||||
|
|
||||||
**One-way (label schema, ownership model application) + two-way (label
|
**One-way (label schema, ownership model application) + two-way (label
|
||||||
prefix, stale-entry policy).** The label schema (two labels,
|
prefix).** The label schema (two labels,
|
||||||
`<prefix>.managed` + `<prefix>.owner`) and the
|
`<prefix>.managed` + `<prefix>.owner`) and the
|
||||||
create-records/remove-revokes ownership coupling are one-way: clients
|
create-records/remove-revokes ownership coupling are one-way: clients
|
||||||
and the ownership store depend on the labels and the record/revoke
|
and the ownership store depend on the labels and the record/revoke
|
||||||
timing. The label prefix (default `alknet`) is two-way-door config. The
|
timing. The label prefix (default `alknet`) is two-way-door config. The
|
||||||
stale-entry policy (tolerate, no reaper) is two-way — a reaper
|
`docker/system/events` subscription provides the prompt-cleanup path
|
||||||
subscription is an additive refinement.
|
for stale entries; the internal ownership-store subscription is a
|
||||||
|
follow-up refinement.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ Door type is separate from whether a decision is made. A two-way door is a decis
|
|||||||
|----|-------|--------|------|-----|
|
|----|-------|--------|------|-----|
|
||||||
| [OQ-48](questions/048-network-and-volume-operation-surface.md) | Network and Volume Operation Surface | deferred(scope) | two | low |
|
| [OQ-48](questions/048-network-and-volume-operation-surface.md) | Network and Volume Operation Surface | deferred(scope) | two | low |
|
||||||
| [OQ-49](questions/049-image-build-buildkit-scope.md) | Image Build (buildkit) Scope | deferred(scope) | two | low |
|
| [OQ-49](questions/049-image-build-buildkit-scope.md) | Image Build (buildkit) Scope | deferred(scope) | two | low |
|
||||||
| [OQ-50](questions/050-docker-system-events-subscription.md) | Docker System Events Subscription | deferred(scope) | two | low |
|
| [OQ-50](questions/050-docker-system-events-subscription.md) | Docker System Events Subscription | resolved | two | low |
|
||||||
| [OQ-51](questions/051-container-create-options-surface.md) | Container Create Options Surface | deferred(scope) | two | med |
|
| [OQ-51](questions/051-container-create-options-surface.md) | Container Create Options Surface | deferred(scope) | two | med |
|
||||||
|
|
||||||
### alknet-hub
|
### alknet-hub
|
||||||
@@ -224,12 +224,6 @@ filtering the tables above.
|
|||||||
- **Priority**: low
|
- **Priority**: low
|
||||||
- **Full file**: [OQ-49](questions/049-image-build-buildkit-scope.md)
|
- **Full file**: [OQ-49](questions/049-image-build-buildkit-scope.md)
|
||||||
|
|
||||||
### OQ-50: Docker System Events Subscription
|
|
||||||
|
|
||||||
- **Blocked on**: a concrete use case for prompt stale-ownership cleanup. The base model (ADR-060 §4) tolerates inert stale entries; the events subscription is a refinement.
|
|
||||||
- **Priority**: low
|
|
||||||
- **Full file**: [OQ-50](questions/050-docker-system-events-subscription.md)
|
|
||||||
|
|
||||||
### OQ-51: Container Create Options Surface
|
### OQ-51: Container Create Options Surface
|
||||||
|
|
||||||
- **Blocked on**: v1 implementation — the `create` input JSON Schema is finalized when `register_docker_ops` is written and tested against bollard's `Config` struct. An architectural decision (ADR-060 §5), not a deferral past implementation.
|
- **Blocked on**: v1 implementation — the `create` input JSON Schema is finalized when `register_docker_ops` is written and tested against bollard's `Config` struct. An architectural decision (ADR-060 §5), not a deferral past implementation.
|
||||||
|
|||||||
@@ -2,33 +2,28 @@
|
|||||||
|
|
||||||
- **Origin**:
|
- **Origin**:
|
||||||
[crates/docker/docker-operations.md](crates/docker/docker-operations.md)
|
[crates/docker/docker-operations.md](crates/docker/docker-operations.md)
|
||||||
§"Out of scope for v1";
|
§"Operation Surface";
|
||||||
[ADR-060](decisions/060-container-resource-model-and-label-namespace.md)
|
[ADR-060](decisions/060-container-resource-model-and-label-namespace.md)
|
||||||
§4 (autonomous-death tolerance).
|
§4 (autonomous-death tolerance).
|
||||||
- **Status**: deferred(scope)
|
- **Status**: resolved
|
||||||
- **Door type**: Two-way
|
- **Door type**: Two-way
|
||||||
- **Priority**: low
|
- **Priority**: low
|
||||||
- **Blocked on**: a concrete use case for prompt stale-ownership
|
- **Resolution**: `docker/system/events` is included in v1 as a
|
||||||
cleanup. The base model (ADR-060 §4) tolerates stale ownership
|
`Subscription` operation. bollard's `events()` returns
|
||||||
entries from autonomous container death (a `--rm` exit, external
|
`Stream<SystemEventsMessage>` — the same `StreamingHandler` pattern
|
||||||
`docker rm`, daemon restart) — they're inert (a reused container ID
|
already wired for `logs`, `exec`, and `image/pull`. The operation
|
||||||
gets a fresh `record` on its next `create`). A reaper that
|
surfaces daemon events (container start/stop/die/destroy, image
|
||||||
subscribes to docker daemon events would clean them promptly, but
|
pull/tag/delete, etc.) as `call.responded` frames. The internal
|
||||||
the promptness gain is marginal for the current use cases.
|
ownership-store subscription for stale-entry cleanup on `destroy`
|
||||||
- **Resolution**: Not yet decidable. bollard's `events()`
|
events is a follow-up refinement — the operation itself is the
|
||||||
(system.rs:128) returns a `Stream<SystemEventsMessage>` of daemon
|
architecture decision. The use case is already documented in
|
||||||
events (container start/stop/die/destroy, image pull, etc.). A
|
ADR-060 §4 (autonomous container death leaves stale ownership
|
||||||
`docker/system/events` `Subscription` operation would surface these
|
entries); the operation is cheap to add (same mechanical
|
||||||
as `call.responded` frames; the ownership store could subscribe
|
`StreamingHandler` mapping as the three existing streaming ops) and
|
||||||
internally to revoke on `destroy` events. The deferral is scope:
|
generally useful beyond ownership cleanup (any consumer may want
|
||||||
the base model works without it; the events subscription is a
|
daemon events).
|
||||||
refinement for when prompt cleanup matters (e.g., a high-churn
|
|
||||||
coordinator that spawns/removes many containers and wants the
|
|
||||||
ownership store to stay tight). Adding it is additive (a new
|
|
||||||
operation + an internal store subscription) and does not break the
|
|
||||||
existing surface.
|
|
||||||
- **Cross-references**:
|
- **Cross-references**:
|
||||||
[ADR-060](decisions/060-container-resource-model-and-label-namespace.md)
|
[ADR-060](decisions/060-container-resource-model-and-label-namespace.md)
|
||||||
§4 (teardown coupling — stale-entry tolerance is the base model
|
§4 (teardown coupling — stale-entry tolerance is the base model;
|
||||||
this subscription would refine),
|
the events subscription provides the prompt-cleanup path),
|
||||||
[crates/docker/docker-operations.md](crates/docker/docker-operations.md)
|
[crates/docker/docker-operations.md](crates/docker/docker-operations.md)
|
||||||
Reference in new issue
Block a user