Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions skills/docker-to-sealos/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ If validation fails, fix template/rules/examples first.

- Do not use `emptyDir`.
- Use persistent storage patterns (`volumeClaimTemplates`) where storage is needed.
- StatefulSet resources with `volumeClaimTemplates` must set `metadata.labels.cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}` and every `volumeClaimTemplates[].metadata.labels.cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}` so Template can track and clean PVCs.
- PVC request must be `<= 1Gi` unless source spec explicitly requires less.
- ConfigMap keys and volume names must follow vn naming (`scripts/path_converter.py`).

Expand Down
7 changes: 7 additions & 0 deletions skills/docker-to-sealos/references/conversion-mappings.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ metadata:
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }}
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
spec:
revisionHistoryLimit: 1
template:
Expand All @@ -375,6 +376,9 @@ spec:
annotations:
path: /app/data
value: '1'
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
name: vn-appvn-data
spec:
accessModes:
Expand All @@ -386,6 +390,9 @@ spec:
annotations:
path: /app/config
value: '1'
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
name: vn-appvn-config
spec:
accessModes:
Expand Down
2 changes: 2 additions & 0 deletions skills/docker-to-sealos/references/example-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,8 @@ For all resources deployed through the template marketplace, including system re

Where `app_name` is the name of the application deployed by the user, which by default ends with a random number, such as `fastgpt-zu1n048s`.

For application `StatefulSet` resources that define `spec.volumeClaimTemplates`, also set `cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}` on every `volumeClaimTemplates[].metadata.labels`. Preserve component labels such as `app` so legacy component-level PVC cleanup remains possible.

## Part 3: `Rendering Process Details`

The Sealos template engine follows a specific order during the rendering process to ensure that variables and conditional statements can be correctly parsed.
Expand Down
6 changes: 5 additions & 1 deletion skills/docker-to-sealos/references/must-rules-map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ must_rules:
enforcement:
type: manual
note: "Requires intent-level storage-need inference for each service."
- must: "StatefulSet resources with `volumeClaimTemplates` must set `metadata.labels.cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}` and every `volumeClaimTemplates[].metadata.labels.cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}` so Template can track and clean PVCs."
enforcement:
type: rule
target: R041
- must: "PVC request must be `<= 1Gi` unless source spec explicitly requires less."
enforcement:
type: rule
Expand Down Expand Up @@ -216,7 +220,7 @@ must_rules:
enforcement:
type: manual
note: "Requires label presence checks scoped to PostgreSQL RBAC resources."
- must: "Every KubeBlocks database `Cluster` must include `kb.io/database`, `sealos-db-provider-cr`, and `clusterdefinition.kubeblocks.io/name` labels; `sealos-db-provider-cr` must equal `metadata.name`."
- must: "Every KubeBlocks database `Cluster` must include `kb.io/database`, `sealos-db-provider-cr`, and `clusterdefinition.kubeblocks.io/name` labels; `sealos-db-provider-cr` must equal `metadata.name` so dbprovider can list and classify the database. Related Pods, Services, and OpsRequests should carry `app.kubernetes.io/instance=<database name>` for detail views."
enforcement:
type: rule
target: R040
Expand Down
3 changes: 3 additions & 0 deletions skills/docker-to-sealos/references/rules-registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ rules:
- id: R011
description: All PVC storage requests must be concrete values and less than or equal to 1Gi.
severity: error
- id: R041
description: StatefulSet resources with volumeClaimTemplates must label the StatefulSet and every volumeClaimTemplate with cloud.sealos.io/deploy-on-sealos for Template PVC tracking.
severity: error
4 changes: 4 additions & 0 deletions skills/docker-to-sealos/references/sealos-specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ volumes:
- For StatefulSet: Use `volumeClaimTemplates` to create persistent storage
- For Deployment: Consider whether storage is truly needed; if so, switch to StatefulSet
- For temporary configuration: Consider using ConfigMap or Secret
- For StatefulSet PVC tracking: set `cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}` on both the StatefulSet metadata labels and every `volumeClaimTemplates[].metadata.labels`, while preserving component labels such as `app`.

### PersistentVolumeClaim Usage Restriction

Expand Down Expand Up @@ -318,6 +319,9 @@ volumeClaimTemplates:
annotations:
path: /var/lib/headscale # Mount path
value: '1' # Fixed value
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
name: vn-varvn-libvn-headscale # Naming rules see below
spec:
accessModes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
NEGATIVE_MARKERS = ("wrong example", "❌", "invalid example")
WORKLOAD_KINDS = {"Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob"}
APP_WORKLOAD_KINDS = {"Deployment", "StatefulSet", "DaemonSet"}
TEMPLATE_DEPLOY_KEY = "cloud.sealos.io/deploy-on-sealos"
DB_SECRET_SUFFIXES = (
"-pg-conn-credential",
"-mysql-conn-credential",
Expand Down
58 changes: 58 additions & 0 deletions skills/docker-to-sealos/scripts/check_consistency_rules_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
ScanContext,
SEALOS_CPU_REQUEST_BY_LIMIT,
SEALOS_MEMORY_REQUEST_BY_LIMIT,
TEMPLATE_DEPLOY_KEY,
Violation,
)
from check_consistency_parser import find_line
Expand Down Expand Up @@ -114,6 +115,62 @@ def check_pvc_storage_limit(context: ScanContext) -> List[Violation]:
return violations


def check_statefulset_template_deploy_labels(context: ScanContext) -> List[Violation]:
violations: List[Violation] = []
expected_value = "${{ defaults.app_name }}"

for doc in context.yaml_documents:
if doc.skip_checks or not isinstance(doc.data, dict):
continue
if doc.data.get("kind") != "StatefulSet":
continue

metadata = doc.data.get("metadata")
labels = metadata.get("labels") if isinstance(metadata, dict) else None
if not isinstance(labels, dict) or labels.get(TEMPLATE_DEPLOY_KEY) != expected_value:
add_doc_violation(
violations,
rule_id="R041",
doc=doc,
pattern=rf"^\s*{re.escape(TEMPLATE_DEPLOY_KEY)}\s*:",
default_pattern=r"^\s*labels\s*:",
message=(
f"StatefulSet metadata.labels.{TEMPLATE_DEPLOY_KEY} "
f"must be {expected_value} for Template instance tracking"
),
)

spec = doc.data.get("spec")
volume_claim_templates = spec.get("volumeClaimTemplates") if isinstance(spec, dict) else None
if not isinstance(volume_claim_templates, list):
continue

for volume_claim_template in volume_claim_templates:
if not isinstance(volume_claim_template, dict):
continue
vct_metadata = volume_claim_template.get("metadata")
vct_labels = vct_metadata.get("labels") if isinstance(vct_metadata, dict) else None
if isinstance(vct_labels, dict) and vct_labels.get(TEMPLATE_DEPLOY_KEY) == expected_value:
continue

name = "<unknown>"
if isinstance(vct_metadata, dict) and isinstance(vct_metadata.get("name"), str):
name = vct_metadata["name"]
add_doc_violation(
violations,
rule_id="R041",
doc=doc,
pattern=rf"^\s*{re.escape(TEMPLATE_DEPLOY_KEY)}\s*:",
default_pattern=rf"^\s*name\s*:\s*{re.escape(name)}\s*$",
message=(
f"StatefulSet volumeClaimTemplates[{name}] metadata.labels.{TEMPLATE_DEPLOY_KEY} "
f"must be {expected_value} so Template can track and clean PVCs"
),
)

return violations


def _display_allowed(values: Dict[str, str]) -> str:
return "/".join(values.keys())

Expand Down Expand Up @@ -407,6 +464,7 @@ def check_database_cluster_visibility_labels(context: ScanContext) -> List[Viola
"R005": Rule("R005", check_no_emptydir),
"R006": Rule("R006", check_image_pull_policy),
"R011": Rule("R011", check_pvc_storage_limit),
"R041": Rule("R041", check_statefulset_template_deploy_labels),
"R019": Rule("R019", check_database_cluster_component_resources),
"R040": Rule("R040", check_database_cluster_visibility_labels),
"R038": Rule("R038", check_managed_workload_resource_ladder),
Expand Down
6 changes: 6 additions & 0 deletions skills/docker-to-sealos/scripts/compose_to_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"BACKEND_STORAGE_MINIO_EXTERNAL_ENDPOINT",
}
OBJECT_STORAGE_BUCKET_ENV_NAME = "S3_BUCKET"
TEMPLATE_DEPLOY_KEY = "cloud.sealos.io/deploy-on-sealos"
COMPOSE_REFERENCE_RE = re.compile(r"\$\{[^}]+\}")
INVALID_NAME_RE = re.compile(r"[^a-z0-9]+")
MODE_SUFFIXES = {"ro", "rw", "z", "Z", "cached", "delegated", "consistent"}
Expand Down Expand Up @@ -2134,6 +2135,10 @@ def build_workload(
"metadata": {
"name": path_to_vn_name(path),
"annotations": {"path": path, "value": "1"},
"labels": {
"app": workload_name,
TEMPLATE_DEPLOY_KEY: "${{ defaults.app_name }}",
},
},
"spec": {
"accessModes": ["ReadWriteOnce"],
Expand All @@ -2155,6 +2160,7 @@ def build_workload(
},
"labels": {
"cloud.sealos.io/app-deploy-manager": workload_name,
TEMPLATE_DEPLOY_KEY: "${{ defaults.app_name }}",
"app": workload_name,
},
},
Expand Down
83 changes: 83 additions & 0 deletions skills/docker-to-sealos/scripts/test_check_consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -2989,6 +2989,89 @@ def test_detects_pvc_storage_variable_expression(self):
)
self.assertTrue(any(item.rule_id == "R011" for item in violations))

def test_detects_statefulset_volume_claim_template_missing_template_deploy_label(self):
violations = self.run_checker(
"""
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: demo
labels:
app: demo
cloud.sealos.io/app-deploy-manager: demo
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
spec:
revisionHistoryLimit: 1
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
automountServiceAccountToken: false
containers:
- name: demo
image: nginx:1.27.2
imagePullPolicy: IfNotPresent
volumeClaimTemplates:
- metadata:
name: data
labels:
app: demo
spec:
resources:
requests:
storage: 1Gi
```
"""
)
self.assertTrue(any(item.rule_id == "R041" for item in violations))

def test_allows_statefulset_volume_claim_template_template_deploy_label(self):
violations = self.run_checker(
"""
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: demo
labels:
app: demo
cloud.sealos.io/app-deploy-manager: demo
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
spec:
revisionHistoryLimit: 1
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
automountServiceAccountToken: false
containers:
- name: demo
image: nginx:1.27.2
imagePullPolicy: IfNotPresent
volumeClaimTemplates:
- metadata:
name: data
labels:
app: demo
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
spec:
resources:
requests:
storage: 1Gi
```
"""
)
self.assertFalse(any(item.rule_id == "R041" for item in violations))

def test_registry_rule_scope_filters_violations(self):
rules_yaml = render_registry(
overrides={
Expand Down
8 changes: 8 additions & 0 deletions skills/docker-to-sealos/scripts/test_compose_to_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,12 +732,20 @@ def test_skips_socket_mount_from_stateful_storage_conversion(self):
)
docs = parse_yaml_documents(index_path)
workload = next(doc for doc in docs if doc.get("kind") == "StatefulSet")
self.assertEqual(
"${{ defaults.app_name }}",
workload["metadata"]["labels"]["cloud.sealos.io/deploy-on-sealos"],
)
mounts = workload["spec"]["template"]["spec"]["containers"][0]["volumeMounts"]
mount_paths = [item["mountPath"] for item in mounts]
self.assertEqual(["/data"], mount_paths)
pvcs = workload["spec"]["volumeClaimTemplates"]
pvc_names = [item["metadata"]["name"] for item in pvcs]
self.assertEqual(["vn-data"], pvc_names)
self.assertEqual(
"${{ defaults.app_name }}",
pvcs[0]["metadata"]["labels"]["cloud.sealos.io/deploy-on-sealos"],
)

def test_rejects_latest_image_tag(self):
with tempfile.TemporaryDirectory() as temp_dir:
Expand Down