feat(docker): schema for lifecycle events + swarm topology
Adds the milestone-77 storage that doesn't fit on the per-container row:
* docker_events — lifecycle (start/stop/die/oom/health_change), to be
derived by diffing consecutive host snapshots; host-scoped, indexed for
timeline lookups (host_id, container_name, at) and retention pruning (at).
* docker_swarm_services / docker_swarm_nodes — manager-reported Swarm
topology (desired-vs-running replicas, node role/availability/status).
Migration docker_004 extends the docker branch (down_revision docker_003);
purely additive, no DROP+recreate. event/mode/role are plain strings (no
CHECK whitelist), matching how docker_containers models status. Integration
guard asserts the three new host-scoped tables exist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
This commit is contained in:
@@ -85,3 +85,89 @@ class DockerMetric(Base):
|
||||
Index("ix_docker_metrics_host_container_time",
|
||||
"host_id", "container_name", "scraped_at"),
|
||||
)
|
||||
|
||||
|
||||
class DockerEvent(Base):
|
||||
"""Lifecycle events derived by diffing consecutive host snapshots.
|
||||
|
||||
The agent only reports current state, so Steward synthesises lifecycle by
|
||||
comparing each new snapshot against the stored DockerContainer state for the
|
||||
host: a container that appears → `start`; one that vanishes → `stop`; a
|
||||
transition to a stopped status with a non-zero exit → `die`; an OOM-kill
|
||||
flip → `oom`; a health status change → `health_change`. Scoped to the
|
||||
reporting host (names are only unique within a host). `event` is a plain
|
||||
string (no CHECK whitelist), matching how `status` is modelled on
|
||||
DockerContainer — keeps the value set open without a migration per addition.
|
||||
"""
|
||||
__tablename__ = "docker_events"
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
host_id: Mapped[str] = mapped_column(
|
||||
String(36), ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
container_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
event: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
# start | stop | die | oom | health_change
|
||||
detail: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
# e.g. "exit 137", "healthy→unhealthy", image on start
|
||||
at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Timeline lookups filter on (host_id, container_name) and sort by time;
|
||||
# retention prunes by `at` alone — index both access paths.
|
||||
__table_args__ = (
|
||||
Index("ix_docker_events_host_container_time",
|
||||
"host_id", "container_name", "at"),
|
||||
Index("ix_docker_events_at", "at"),
|
||||
)
|
||||
|
||||
|
||||
class DockerSwarmService(Base):
|
||||
"""A Swarm service as seen by a manager host: desired vs running replicas.
|
||||
|
||||
Manager-scoped — only a host that is a Swarm manager reports these, so a
|
||||
single-manager cluster yields one row set keyed by that host. Service names
|
||||
are unique within a swarm; we still key by (host_id, service_name) so two
|
||||
managers reporting independently don't collide.
|
||||
"""
|
||||
__tablename__ = "docker_swarm_services"
|
||||
|
||||
host_id: Mapped[str] = mapped_column(
|
||||
String(36), ForeignKey("hosts.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
service_name: Mapped[str] = mapped_column(String(255), primary_key=True)
|
||||
mode: Mapped[str] = mapped_column(String(16), nullable=False, default="replicated")
|
||||
# replicated | global
|
||||
desired: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
running: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
image: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
class DockerSwarmNode(Base):
|
||||
"""A Swarm node as seen by a manager host: role / availability / status."""
|
||||
__tablename__ = "docker_swarm_nodes"
|
||||
|
||||
host_id: Mapped[str] = mapped_column(
|
||||
String(36), ForeignKey("hosts.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
node_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
hostname: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||
role: Mapped[str] = mapped_column(String(16), nullable=False, default="worker")
|
||||
# manager | worker
|
||||
availability: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||
# active | pause | drain
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="unknown")
|
||||
# ready | down | unknown
|
||||
leader: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user