Skip to content

Commit 36ef7cd

Browse files
authored
fix(docker-to-sealos): label StatefulSet PVCs (#37)
1 parent cf5b798 commit 36ef7cd

11 files changed

Lines changed: 178 additions & 1 deletion

skills/docker-to-sealos/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ If validation fails, fix template/rules/examples first.
148148

149149
- Do not use `emptyDir`.
150150
- Use persistent storage patterns (`volumeClaimTemplates`) where storage is needed.
151+
- 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.
151152
- PVC request must be `<= 1Gi` unless source spec explicitly requires less.
152153
- ConfigMap keys and volume names must follow vn naming (`scripts/path_converter.py`).
153154

skills/docker-to-sealos/references/conversion-mappings.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ metadata:
358358
labels:
359359
app: ${{ defaults.app_name }}
360360
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }}
361+
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
361362
spec:
362363
revisionHistoryLimit: 1
363364
template:
@@ -375,6 +376,9 @@ spec:
375376
annotations:
376377
path: /app/data
377378
value: '1'
379+
labels:
380+
app: ${{ defaults.app_name }}
381+
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
378382
name: vn-appvn-data
379383
spec:
380384
accessModes:
@@ -386,6 +390,9 @@ spec:
386390
annotations:
387391
path: /app/config
388392
value: '1'
393+
labels:
394+
app: ${{ defaults.app_name }}
395+
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
389396
name: vn-appvn-config
390397
spec:
391398
accessModes:

skills/docker-to-sealos/references/example-guide.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,6 +1504,8 @@ For all resources deployed through the template marketplace, including system re
15041504

15051505
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`.
15061506

1507+
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.
1508+
15071509
## Part 3: `Rendering Process Details`
15081510

15091511
The Sealos template engine follows a specific order during the rendering process to ensure that variables and conditional statements can be correctly parsed.

skills/docker-to-sealos/references/must-rules-map.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ must_rules:
152152
enforcement:
153153
type: manual
154154
note: "Requires intent-level storage-need inference for each service."
155+
- 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."
156+
enforcement:
157+
type: rule
158+
target: R041
155159
- must: "PVC request must be `<= 1Gi` unless source spec explicitly requires less."
156160
enforcement:
157161
type: rule
@@ -216,7 +220,7 @@ must_rules:
216220
enforcement:
217221
type: manual
218222
note: "Requires label presence checks scoped to PostgreSQL RBAC resources."
219-
- 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`."
223+
- 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."
220224
enforcement:
221225
type: rule
222226
target: R040

skills/docker-to-sealos/references/rules-registry.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,6 @@ rules:
127127
- id: R011
128128
description: All PVC storage requests must be concrete values and less than or equal to 1Gi.
129129
severity: error
130+
- id: R041
131+
description: StatefulSet resources with volumeClaimTemplates must label the StatefulSet and every volumeClaimTemplate with cloud.sealos.io/deploy-on-sealos for Template PVC tracking.
132+
severity: error

skills/docker-to-sealos/references/sealos-specs.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ volumes:
290290
- For StatefulSet: Use `volumeClaimTemplates` to create persistent storage
291291
- For Deployment: Consider whether storage is truly needed; if so, switch to StatefulSet
292292
- For temporary configuration: Consider using ConfigMap or Secret
293+
- 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`.
293294

294295
### PersistentVolumeClaim Usage Restriction
295296

@@ -318,6 +319,9 @@ volumeClaimTemplates:
318319
annotations:
319320
path: /var/lib/headscale # Mount path
320321
value: '1' # Fixed value
322+
labels:
323+
app: ${{ defaults.app_name }}
324+
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
321325
name: vn-varvn-libvn-headscale # Naming rules see below
322326
spec:
323327
accessModes:

skills/docker-to-sealos/scripts/check_consistency_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
NEGATIVE_MARKERS = ("wrong example", "❌", "invalid example")
1818
WORKLOAD_KINDS = {"Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob"}
1919
APP_WORKLOAD_KINDS = {"Deployment", "StatefulSet", "DaemonSet"}
20+
TEMPLATE_DEPLOY_KEY = "cloud.sealos.io/deploy-on-sealos"
2021
DB_SECRET_SUFFIXES = (
2122
"-pg-conn-credential",
2223
"-mysql-conn-credential",

skills/docker-to-sealos/scripts/check_consistency_rules_storage.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
ScanContext,
1515
SEALOS_CPU_REQUEST_BY_LIMIT,
1616
SEALOS_MEMORY_REQUEST_BY_LIMIT,
17+
TEMPLATE_DEPLOY_KEY,
1718
Violation,
1819
)
1920
from check_consistency_parser import find_line
@@ -114,6 +115,62 @@ def check_pvc_storage_limit(context: ScanContext) -> List[Violation]:
114115
return violations
115116

116117

118+
def check_statefulset_template_deploy_labels(context: ScanContext) -> List[Violation]:
119+
violations: List[Violation] = []
120+
expected_value = "${{ defaults.app_name }}"
121+
122+
for doc in context.yaml_documents:
123+
if doc.skip_checks or not isinstance(doc.data, dict):
124+
continue
125+
if doc.data.get("kind") != "StatefulSet":
126+
continue
127+
128+
metadata = doc.data.get("metadata")
129+
labels = metadata.get("labels") if isinstance(metadata, dict) else None
130+
if not isinstance(labels, dict) or labels.get(TEMPLATE_DEPLOY_KEY) != expected_value:
131+
add_doc_violation(
132+
violations,
133+
rule_id="R041",
134+
doc=doc,
135+
pattern=rf"^\s*{re.escape(TEMPLATE_DEPLOY_KEY)}\s*:",
136+
default_pattern=r"^\s*labels\s*:",
137+
message=(
138+
f"StatefulSet metadata.labels.{TEMPLATE_DEPLOY_KEY} "
139+
f"must be {expected_value} for Template instance tracking"
140+
),
141+
)
142+
143+
spec = doc.data.get("spec")
144+
volume_claim_templates = spec.get("volumeClaimTemplates") if isinstance(spec, dict) else None
145+
if not isinstance(volume_claim_templates, list):
146+
continue
147+
148+
for volume_claim_template in volume_claim_templates:
149+
if not isinstance(volume_claim_template, dict):
150+
continue
151+
vct_metadata = volume_claim_template.get("metadata")
152+
vct_labels = vct_metadata.get("labels") if isinstance(vct_metadata, dict) else None
153+
if isinstance(vct_labels, dict) and vct_labels.get(TEMPLATE_DEPLOY_KEY) == expected_value:
154+
continue
155+
156+
name = "<unknown>"
157+
if isinstance(vct_metadata, dict) and isinstance(vct_metadata.get("name"), str):
158+
name = vct_metadata["name"]
159+
add_doc_violation(
160+
violations,
161+
rule_id="R041",
162+
doc=doc,
163+
pattern=rf"^\s*{re.escape(TEMPLATE_DEPLOY_KEY)}\s*:",
164+
default_pattern=rf"^\s*name\s*:\s*{re.escape(name)}\s*$",
165+
message=(
166+
f"StatefulSet volumeClaimTemplates[{name}] metadata.labels.{TEMPLATE_DEPLOY_KEY} "
167+
f"must be {expected_value} so Template can track and clean PVCs"
168+
),
169+
)
170+
171+
return violations
172+
173+
117174
def _display_allowed(values: Dict[str, str]) -> str:
118175
return "/".join(values.keys())
119176

@@ -407,6 +464,7 @@ def check_database_cluster_visibility_labels(context: ScanContext) -> List[Viola
407464
"R005": Rule("R005", check_no_emptydir),
408465
"R006": Rule("R006", check_image_pull_policy),
409466
"R011": Rule("R011", check_pvc_storage_limit),
467+
"R041": Rule("R041", check_statefulset_template_deploy_labels),
410468
"R019": Rule("R019", check_database_cluster_component_resources),
411469
"R040": Rule("R040", check_database_cluster_visibility_labels),
412470
"R038": Rule("R038", check_managed_workload_resource_ladder),

skills/docker-to-sealos/scripts/compose_to_template.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"BACKEND_STORAGE_MINIO_EXTERNAL_ENDPOINT",
7272
}
7373
OBJECT_STORAGE_BUCKET_ENV_NAME = "S3_BUCKET"
74+
TEMPLATE_DEPLOY_KEY = "cloud.sealos.io/deploy-on-sealos"
7475
COMPOSE_REFERENCE_RE = re.compile(r"\$\{[^}]+\}")
7576
INVALID_NAME_RE = re.compile(r"[^a-z0-9]+")
7677
MODE_SUFFIXES = {"ro", "rw", "z", "Z", "cached", "delegated", "consistent"}
@@ -2134,6 +2135,10 @@ def build_workload(
21342135
"metadata": {
21352136
"name": path_to_vn_name(path),
21362137
"annotations": {"path": path, "value": "1"},
2138+
"labels": {
2139+
"app": workload_name,
2140+
TEMPLATE_DEPLOY_KEY: "${{ defaults.app_name }}",
2141+
},
21372142
},
21382143
"spec": {
21392144
"accessModes": ["ReadWriteOnce"],
@@ -2155,6 +2160,7 @@ def build_workload(
21552160
},
21562161
"labels": {
21572162
"cloud.sealos.io/app-deploy-manager": workload_name,
2163+
TEMPLATE_DEPLOY_KEY: "${{ defaults.app_name }}",
21582164
"app": workload_name,
21592165
},
21602166
},

skills/docker-to-sealos/scripts/test_check_consistency.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2989,6 +2989,89 @@ def test_detects_pvc_storage_variable_expression(self):
29892989
)
29902990
self.assertTrue(any(item.rule_id == "R011" for item in violations))
29912991

2992+
def test_detects_statefulset_volume_claim_template_missing_template_deploy_label(self):
2993+
violations = self.run_checker(
2994+
"""
2995+
```yaml
2996+
apiVersion: apps/v1
2997+
kind: StatefulSet
2998+
metadata:
2999+
name: demo
3000+
labels:
3001+
app: demo
3002+
cloud.sealos.io/app-deploy-manager: demo
3003+
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
3004+
spec:
3005+
revisionHistoryLimit: 1
3006+
selector:
3007+
matchLabels:
3008+
app: demo
3009+
template:
3010+
metadata:
3011+
labels:
3012+
app: demo
3013+
spec:
3014+
automountServiceAccountToken: false
3015+
containers:
3016+
- name: demo
3017+
image: nginx:1.27.2
3018+
imagePullPolicy: IfNotPresent
3019+
volumeClaimTemplates:
3020+
- metadata:
3021+
name: data
3022+
labels:
3023+
app: demo
3024+
spec:
3025+
resources:
3026+
requests:
3027+
storage: 1Gi
3028+
```
3029+
"""
3030+
)
3031+
self.assertTrue(any(item.rule_id == "R041" for item in violations))
3032+
3033+
def test_allows_statefulset_volume_claim_template_template_deploy_label(self):
3034+
violations = self.run_checker(
3035+
"""
3036+
```yaml
3037+
apiVersion: apps/v1
3038+
kind: StatefulSet
3039+
metadata:
3040+
name: demo
3041+
labels:
3042+
app: demo
3043+
cloud.sealos.io/app-deploy-manager: demo
3044+
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
3045+
spec:
3046+
revisionHistoryLimit: 1
3047+
selector:
3048+
matchLabels:
3049+
app: demo
3050+
template:
3051+
metadata:
3052+
labels:
3053+
app: demo
3054+
spec:
3055+
automountServiceAccountToken: false
3056+
containers:
3057+
- name: demo
3058+
image: nginx:1.27.2
3059+
imagePullPolicy: IfNotPresent
3060+
volumeClaimTemplates:
3061+
- metadata:
3062+
name: data
3063+
labels:
3064+
app: demo
3065+
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
3066+
spec:
3067+
resources:
3068+
requests:
3069+
storage: 1Gi
3070+
```
3071+
"""
3072+
)
3073+
self.assertFalse(any(item.rule_id == "R041" for item in violations))
3074+
29923075
def test_registry_rule_scope_filters_violations(self):
29933076
rules_yaml = render_registry(
29943077
overrides={

0 commit comments

Comments
 (0)