Skip to content

Commit 9f2f828

Browse files
fix(docker-to-sealos): block license-gated s3 options (#70)
1 parent 387d2ea commit 9f2f828

7 files changed

Lines changed: 247 additions & 0 deletions

File tree

skills/docker-to-sealos/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ For login-gated web applications, live validation must prove the real credential
180180
- Omit ConfigMap volume `defaultMode` unless the application explicitly needs a non-default mode. ConfigMap scripts invoked through `/bin/sh /path/script` do not need executable bits.
181181
- Avoid long inline startup scripts or heredocs in `command`/`args`; place initialization/start scripts in ConfigMap files and invoke them with a short command.
182182
- When object storage is required and Sealos can satisfy it, create `ObjectStorageBucket` and inject Sealos object-storage secrets; managed Sealos toggles such as `use_sealos_objectstorage` may control the `ObjectStorageBucket` branch; external S3/object-storage credential inputs require `metadata.annotations.docker-to-sealos.external-object-storage-source` evidence, and must not coexist with `ObjectStorageBucket`.
183+
- If object storage/S3 integration is Enterprise, paid, commercial, subscription, or license-gated in the upstream application, keep the public template on the community-supported storage path (for example filesystem/PVC) and expose no standard `ObjectStorageBucket` or S3 input for that feature.
183184

184185
### Env and secrets
185186

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,8 @@ When docs offer local file storage and S3-compatible object storage as a binary
903903

904904
When an app needs S3-compatible storage, prefer Sealos `ObjectStorageBucket` and inject the managed object-storage secrets into the app. Preserve managed Sealos toggles such as `use_sealos_objectstorage` when they control an optional `ObjectStorageBucket` branch. Expose external S3/object-storage credential inputs only when source docs or the user require an externally managed bucket, and record that evidence in `metadata.annotations.docker-to-sealos.external-object-storage-source`. Do not combine external S3 credential inputs with a managed `ObjectStorageBucket`.
905905

906+
Before adding Sealos ObjectStorage for an application feature, verify the upstream edition and license requirements. When S3/external object storage support is Enterprise, paid, commercial, subscription, or license-gated, the public template must use the community-supported storage path (usually filesystem/PVC) and must not expose a standard deployment input that provisions `ObjectStorageBucket` or injects S3 env vars for that feature. Add an enterprise-specific template branch only when the user explicitly requests that scope.
907+
906908
### Docker Compose (Using Minio)
907909
```yaml
908910
services:

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ must_rules:
216216
enforcement:
217217
type: rule
218218
target: R047
219+
- must: "If object storage/S3 integration is Enterprise, paid, commercial, subscription, or license-gated in the upstream application, keep the public template on the community-supported storage path (for example filesystem/PVC) and expose no standard `ObjectStorageBucket` or S3 input for that feature."
220+
enforcement:
221+
type: rule
222+
target: R049
219223
- must: "Non-database sensitive values/inputs use direct `env[].value`."
220224
enforcement:
221225
type: manual

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,6 @@ rules:
148148
- id: R011
149149
description: All PVC storage requests must be concrete values and less than or equal to 1Gi.
150150
severity: error
151+
- id: R049
152+
description: Public templates must not expose standard ObjectStorage/S3 inputs for features that require Enterprise, paid, commercial, subscription, or license-gated upstream editions.
153+
severity: error

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,8 @@ metadata:
593593

594594
## Object Storage Configuration
595595

596+
Use ObjectStorage only for application features available in the upstream self-hosted/community edition. If S3/object-storage support requires Enterprise, paid, commercial, subscription, or license activation, the public template must keep the supported filesystem/PVC storage path and must not expose an `ObjectStorageBucket` or S3 toggle for that feature.
597+
596598
### Environment Variable Settings
597599

598600
Object storage environment variable configuration must follow this format:

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

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@
134134
MAIN_CONTAINER_MAX_SCRIPT_CHARS = 160
135135
MAIN_CONTAINER_MAX_SCRIPT_COMMANDS = 2
136136
CONFIGMAP_DATA_KEY_RE = re.compile(r"^vn-[a-z0-9]+(?:vn-[a-z0-9]+)*$")
137+
OBJECT_STORAGE_INPUT_TEXT_RE = re.compile(
138+
r"\b(?:object\s*storage|objectstorage|s3|s3-compatible|bucket|binary\s+data|external\s+storage)\b",
139+
re.IGNORECASE,
140+
)
141+
LICENSE_GATED_TEXT_RE = re.compile(
142+
r"\b(?:enterprise|paid|commercial|premium|subscription|license|licensed|licence|licenced)\b",
143+
re.IGNORECASE,
144+
)
137145
OBJECT_STORAGE_BRANCH_MARKER_RE = re.compile(
138146
r"\b(?:ObjectStorageBucket|object-storage-key|object\s+storage|s3[_-]|aws_access_key_id|"
139147
r"aws_secret_access_key|storage_s3|s3-compatible|bucket|bucket_name|minio)\b",
@@ -152,9 +160,13 @@
152160
"ENABLE_S3_STORAGE",
153161
"ENABLE_SEALOS_OBJECT_STORAGE",
154162
"ENABLE_SEALOS_OBJECTSTORAGE",
163+
"ENABLE_S3",
155164
"USE_OBJECT_STORAGE",
165+
"USE_MANAGED_OBJECT_STORAGE",
166+
"USE_MANAGED_S3",
156167
"USE_SEALOS_OBJECT_STORAGE",
157168
"USE_SEALOS_OBJECTSTORAGE",
169+
"USE_SEALOS_S3",
158170
}
159171
TEMPLATE_IF_RE = re.compile(r"\$\{\{\s*if\s*\((.*?)\)\s*\}\}")
160172
TEMPLATE_ENDIF_RE = re.compile(r"\$\{\{\s*endif\(\)\s*\}\}")
@@ -2295,6 +2307,105 @@ def _external_object_storage_source(doc: YamlDocument) -> str:
22952307
return value.strip() if isinstance(value, str) else ""
22962308

22972309

2310+
def _iter_template_inputs(spec: Dict[str, Any]) -> Iterable[Tuple[str, Any]]:
2311+
inputs = spec.get("inputs")
2312+
if isinstance(inputs, dict):
2313+
for name, input_spec in inputs.items():
2314+
if isinstance(name, str):
2315+
yield name, input_spec
2316+
return
2317+
if isinstance(inputs, list):
2318+
for item in inputs:
2319+
if not isinstance(item, dict):
2320+
continue
2321+
name = item.get("name")
2322+
if isinstance(name, str):
2323+
yield name, item
2324+
2325+
2326+
def _template_input_text(name: str, input_spec: Any) -> str:
2327+
parts = [name]
2328+
if isinstance(input_spec, dict):
2329+
for key in ("label", "title", "description", "default", "value"):
2330+
value = input_spec.get(key)
2331+
if isinstance(value, str):
2332+
parts.append(value)
2333+
elif isinstance(input_spec, str):
2334+
parts.append(input_spec)
2335+
return "\n".join(parts)
2336+
2337+
2338+
def _object_storage_branch_inputs_by_path(context: ScanContext) -> Dict[Path, set[str]]:
2339+
inputs_by_path: Dict[Path, set[str]] = {}
2340+
for path in _iter_template_artifact_paths(context):
2341+
text = context.file_texts.get(path, "")
2342+
lines = text.splitlines()
2343+
for index, line in enumerate(lines):
2344+
match = TEMPLATE_IF_RE.search(line)
2345+
if match is None:
2346+
continue
2347+
input_names = _condition_input_refs(match.group(1))
2348+
if not input_names:
2349+
continue
2350+
branch_end = _find_branch_end(lines, index)
2351+
branch_text = "\n".join(lines[index: branch_end + 1])
2352+
if not _branch_uses_object_storage(branch_text):
2353+
continue
2354+
inputs_by_path.setdefault(path, set()).update(input_names)
2355+
return inputs_by_path
2356+
2357+
2358+
def _input_declaration_line(doc: YamlDocument, input_name: str) -> int:
2359+
escaped = re.escape(input_name)
2360+
inputs_line = doc.line_locator.find(r"^\s*inputs\s*:", default=doc.start_line)
2361+
list_name_line = doc.line_locator.find(
2362+
rf"^\s*name\s*:\s*['\"]?{escaped}['\"]?\s*$",
2363+
default=inputs_line,
2364+
)
2365+
return doc.line_locator.find(rf"^\s*{escaped}\s*:", default=list_name_line)
2366+
2367+
2368+
def check_license_gated_object_storage_options(context: ScanContext) -> List[Violation]:
2369+
violations: List[Violation] = []
2370+
object_storage_branch_inputs = _object_storage_branch_inputs_by_path(context)
2371+
2372+
for doc in _iter_template_artifact_documents(context):
2373+
spec = doc.data.get("spec") if isinstance(doc.data, dict) else None
2374+
if not isinstance(spec, dict):
2375+
continue
2376+
branch_inputs = object_storage_branch_inputs.get(doc.path, set())
2377+
2378+
for input_name, input_spec in _iter_template_inputs(spec):
2379+
input_text = _template_input_text(input_name, input_spec)
2380+
if LICENSE_GATED_TEXT_RE.search(input_text) is None:
2381+
continue
2382+
2383+
normalized_name = _normalize_template_input_name(input_name)
2384+
is_object_storage_input = (
2385+
input_name in branch_inputs
2386+
or normalized_name in MANAGED_OBJECT_STORAGE_TOGGLE_NAMES
2387+
or EXTERNAL_OBJECT_STORAGE_INPUT_RE.search(normalized_name) is not None
2388+
or OBJECT_STORAGE_INPUT_TEXT_RE.search(input_text) is not None
2389+
)
2390+
if not is_object_storage_input:
2391+
continue
2392+
2393+
violations.append(
2394+
Violation(
2395+
rule_id="R049",
2396+
path=doc.path,
2397+
line=_input_declaration_line(doc, input_name),
2398+
message=(
2399+
"license-gated object storage/S3 features must not be exposed as standard "
2400+
"public-template inputs; use the community-supported filesystem/PVC mode "
2401+
"or create a dedicated enterprise template only when explicitly requested"
2402+
),
2403+
)
2404+
)
2405+
2406+
return violations
2407+
2408+
22982409
def check_external_object_storage_inputs(context: ScanContext) -> List[Violation]:
22992410
violations: List[Violation] = []
23002411
object_storage_paths = {
@@ -3095,6 +3206,7 @@ def _container_requires_image_pull_secret(container: Dict[str, Any]) -> bool:
30953206
"R044": Rule("R044", check_optional_object_storage_uses_boolean_input),
30963207
"R045": Rule("R045", check_template_input_references_declared),
30973208
"R047": Rule("R047", check_external_object_storage_inputs),
3209+
"R049": Rule("R049", check_license_gated_object_storage_options),
30983210
"R031": Rule("R031", check_ingress_name_matches_backends),
30993211
"R026": Rule("R026", check_http_ingress_annotations),
31003212
"R048": Rule("R048", check_websocket_ingress_annotations),

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

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3657,6 +3657,129 @@ def test_allows_managed_sealos_objectstorage_toggle_with_object_storage_bucket(s
36573657
)
36583658
self.assertFalse(any(item.rule_id == "R047" for item in violations))
36593659

3660+
def test_detects_license_gated_managed_object_storage_toggle(self):
3661+
with tempfile.TemporaryDirectory() as temp_dir:
3662+
root = Path(temp_dir)
3663+
skill = root / "SKILL.md"
3664+
refs_dir = root / "references"
3665+
refs_file = refs_dir / "sample.md"
3666+
rules_file = refs_dir / "rules-registry.yaml"
3667+
artifact_file = root / "template" / "demo" / "index.yaml"
3668+
3669+
write_file(skill, "# no yaml snippets\n")
3670+
write_file(refs_file, "# refs\n")
3671+
write_registry(rules_file)
3672+
write_file(
3673+
artifact_file,
3674+
"""
3675+
apiVersion: app.sealos.io/v1
3676+
kind: Template
3677+
metadata:
3678+
name: demo
3679+
spec:
3680+
title: Demo
3681+
url: https://example.com
3682+
gitRepo: https://github.com/example/demo
3683+
author: example
3684+
description: demo
3685+
icon: https://raw.githubusercontent.com/labring-actions/templates/kb-0.9/template/demo/logo.png
3686+
templateType: inline
3687+
locale: en
3688+
readme: https://raw.githubusercontent.com/labring-actions/templates/kb-0.9/template/demo/README.md
3689+
i18n:
3690+
zh:
3691+
description: demo
3692+
readme: https://raw.githubusercontent.com/labring-actions/templates/kb-0.9/template/demo/README_zh.md
3693+
categories:
3694+
- tool
3695+
inputs:
3696+
use_sealos_objectstorage:
3697+
description: Provision Sealos ObjectStorage for Enterprise S3 binary data storage. Requires a valid license.
3698+
type: boolean
3699+
default: 'false'
3700+
---
3701+
${{ if(inputs.use_sealos_objectstorage === 'true') }}
3702+
apiVersion: objectstorage.sealos.io/v1
3703+
kind: ObjectStorageBucket
3704+
metadata:
3705+
name: ${{ defaults.app_name }}
3706+
spec:
3707+
policy: private
3708+
${{ endif() }}
3709+
""",
3710+
)
3711+
3712+
violations = CHECKER.run_checks(
3713+
skill,
3714+
refs_dir,
3715+
rules_file,
3716+
additional_include_paths=["template/demo/index.yaml"],
3717+
)
3718+
3719+
self.assertTrue(any(item.rule_id == "R049" for item in violations))
3720+
self.assertTrue(any("license-gated" in item.message for item in violations if item.rule_id == "R049"))
3721+
3722+
def test_allows_community_managed_object_storage_toggle(self):
3723+
with tempfile.TemporaryDirectory() as temp_dir:
3724+
root = Path(temp_dir)
3725+
skill = root / "SKILL.md"
3726+
refs_dir = root / "references"
3727+
refs_file = refs_dir / "sample.md"
3728+
rules_file = refs_dir / "rules-registry.yaml"
3729+
artifact_file = root / "template" / "demo" / "index.yaml"
3730+
3731+
write_file(skill, "# no yaml snippets\n")
3732+
write_file(refs_file, "# refs\n")
3733+
write_registry(rules_file)
3734+
write_file(
3735+
artifact_file,
3736+
"""
3737+
apiVersion: app.sealos.io/v1
3738+
kind: Template
3739+
metadata:
3740+
name: demo
3741+
spec:
3742+
title: Demo
3743+
url: https://example.com
3744+
gitRepo: https://github.com/example/demo
3745+
author: example
3746+
description: demo
3747+
icon: https://raw.githubusercontent.com/labring-actions/templates/kb-0.9/template/demo/logo.png
3748+
templateType: inline
3749+
locale: en
3750+
readme: https://raw.githubusercontent.com/labring-actions/templates/kb-0.9/template/demo/README.md
3751+
i18n:
3752+
zh:
3753+
description: demo
3754+
readme: https://raw.githubusercontent.com/labring-actions/templates/kb-0.9/template/demo/README_zh.md
3755+
categories:
3756+
- tool
3757+
inputs:
3758+
use_sealos_objectstorage:
3759+
description: Provision Sealos ObjectStorage for S3-compatible file uploads.
3760+
type: boolean
3761+
default: 'false'
3762+
---
3763+
${{ if(inputs.use_sealos_objectstorage === 'true') }}
3764+
apiVersion: objectstorage.sealos.io/v1
3765+
kind: ObjectStorageBucket
3766+
metadata:
3767+
name: ${{ defaults.app_name }}
3768+
spec:
3769+
policy: private
3770+
${{ endif() }}
3771+
""",
3772+
)
3773+
3774+
violations = CHECKER.run_checks(
3775+
skill,
3776+
refs_dir,
3777+
rules_file,
3778+
additional_include_paths=["template/demo/index.yaml"],
3779+
)
3780+
3781+
self.assertFalse(any(item.rule_id == "R049" for item in violations))
3782+
36603783
def test_detects_external_s3_inputs_without_source_annotation(self):
36613784
with tempfile.TemporaryDirectory() as temp_dir:
36623785
root = Path(temp_dir)

0 commit comments

Comments
 (0)