# -*- coding: utf-8 -*- """MinIO 对象存储桶 + 生命周期策略模板化 —— issue #31 / PRD 5.2。 在 #4(EPIC)templating 的 MinIO 命名(桶/对象键)之上,交付**完整的 MinIO 对象存储组件**: - **桶**:`{template}-artifacts`(S3 桶名允许 `-`,无需 SQL 清洗), 对象键沿用 templating:特征快照 `features/{model_id}/{date}/{seq}.jsonl`、 模型 artifact `models/{model_id}/{version}/model.bin`; - **生命周期策略**:按前缀配置对象过期天数(S3 LifecycleConfiguration 兼容, MinIO 支持 JSON 形式),如特征快照保留 180 天、模型 artifact 长期保留, 与数据保留/归档策略(#33)联动,避免对象无限堆积; - **落地方式**:只产出**配置文本**(`mc mb` / `mc ilm` 命令 + 生命周期 策略 JSON),不依赖 minio SDK,供模板配置台预览 / 运维执行; - **换行业只改配置**:模板 YAML(`config/minio.template.yaml`)驱动, 内核代码零改动。 验收口径(issue #31 / EPIC #4): - 桶命名模板化(`{template}-{suffix}`),配置可覆盖; - 生命周期规则按前缀声明(ID / 前缀 / 过期天数),生成可执行命令与策略 JSON; - 缺省配置覆盖特征快照 + 模型 artifact 两类对象。 """ from __future__ import annotations import json import os from typing import Dict, List, Optional from data_bus.templating import TemplateNaming #: 默认配置资产路径(相对本模块) DEFAULT_CONFIG_PATH = os.path.join( os.path.dirname(os.path.abspath(__file__)), "config", "minio.template.yaml") def _normalize_rules(lifecycle: Optional[List[dict]]) -> List[dict]: """规范化生命周期规则:过滤非法行,缺省 ID 自动生成。 每条规则形如 ``{"id": ..., "prefix": ..., "expiration_days": ...}``; - prefix 必填(生命周期按对象键前缀匹配); - expiration_days 为过期天数(≥ 1); - 缺省 id 用前缀派生(保证 S3 Rule ID 唯一且稳定)。 """ rules: List[dict] = [] for r in (lifecycle or []): prefix = str(r.get("prefix") or "").strip() days = int(r.get("expiration_days") or 0) if not prefix or days < 1: continue rule_id = str(r.get("id") or "").strip() or f"{prefix}expire" rules.append({ "id": rule_id, "prefix": prefix, "expiration_days": days, }) return rules class MinioLifecycle: """MinIO 桶 + 生命周期策略模板化(模板 + 前缀维度)。""" def __init__( self, template: str, bucket_suffix: str = "artifacts", lifecycle: Optional[List[dict]] = None, mc_alias: str = "local", ) -> None: """ Args: template: 行业模板名(如 ti-cl4 / resin); bucket_suffix: 桶后缀(缺省 artifacts → `{template}-artifacts`); lifecycle: 生命周期规则 `[{"prefix": "features/", "expiration_days": 180}]`; mc_alias: `mc` 命令的远端别名(联调 local / 生产 cluster)。 """ self._tpl = TemplateNaming(template, bucket_suffix=bucket_suffix) self.bucket_suffix = self._tpl.bucket_suffix self.mc_alias = mc_alias self._rules = _normalize_rules(lifecycle) # ------------------------------------------------------------------ @classmethod def from_template_config(cls, path: str = DEFAULT_CONFIG_PATH) -> "MinioLifecycle": """从模板配置资产加载(config/minio.template.yaml)。""" import yaml with open(path, "r", encoding="utf-8") as fh: raw = yaml.safe_load(fh) or {} m = raw.get("minio", {}) or {} return cls( template=str(raw.get("template", "default")), bucket_suffix=str(m.get("bucket_suffix", "artifacts")), lifecycle=m.get("lifecycle", []), mc_alias=str(m.get("mc_alias", "local")), ) # ------------------------------------------------------------------ # 命名:桶 / 对象键(复用 templating,与 #4 命名一致) # ------------------------------------------------------------------ def bucket(self) -> str: """对象存储桶:`{template}-{bucket_suffix}`(S3 桶名允许 `-`)。""" return self._tpl.bucket() def snapshot_key(self, model_id: str, date: str, seq: int) -> str: """特征快照对象键:`features/{model_id}/{date}/{seq:06d}.jsonl`。""" return self._tpl.snapshot_key(model_id, date, seq) def model_artifact_key(self, model_id: str, version: str) -> str: """模型 artifact 对象键:`models/{model_id}/{version}/model.bin`。""" return self._tpl.model_artifact_key(model_id, version) # ------------------------------------------------------------------ # 落地:mc 命令 + 生命周期策略 JSON(只产配置文本,不依赖 SDK) # ------------------------------------------------------------------ def bucket_create_statements(self) -> List[str]: """桶创建命令(`mc mb`,--ignore-existing 幂等)。""" return [f"mc mb --ignore-existing {self.mc_alias}/{self.bucket()}"] def lifecycle_cli_statements(self) -> List[str]: """生命周期规则落地命令(`mc ilm add`,幂等可重跑)。 示例:`mc ilm add --expiry-days 180 --prefix "features/" local/ti-cl4-artifacts` """ bucket = self.bucket() return [ f'mc ilm add --expiry-days {r["expiration_days"]} ' f'--prefix "{r["prefix"]}" {self.mc_alias}/{bucket}' for r in self._rules ] def lifecycle_policy_json(self, indent: int = 2) -> str: """S3 LifecycleConfiguration 兼容的 JSON 策略(MinIO 支持)。 返回结构:``{"Rules": [{"ID", "Status", "Filter": {"Prefix"}, "Expiration": {"Days"}}]}``,可直接用于 ``PutBucketLifecycleConfiguration`` 或配置台预览。 """ rules = [ { "ID": r["id"], "Status": "Enabled", "Filter": {"Prefix": r["prefix"]}, "Expiration": {"Days": r["expiration_days"]}, } for r in self._rules ] return json.dumps({"Rules": rules}, indent=indent, ensure_ascii=False) def brief(self) -> dict: """配置摘要(部署/巡检用)。""" return { "template": self._tpl.template, "bucket": self.bucket(), "mc_alias": self.mc_alias, "lifecycle_rules": list(self._rules), } @property def rules(self) -> List[dict]: """规范化后的生命周期规则(只读视图)。""" return list(self._rules)