stratactl 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. stratactl-1.0.0/PKG-INFO +119 -0
  2. stratactl-1.0.0/README.md +85 -0
  3. stratactl-1.0.0/pyproject.toml +73 -0
  4. stratactl-1.0.0/setup.cfg +4 -0
  5. stratactl-1.0.0/src/strata/__init__.py +8 -0
  6. stratactl-1.0.0/src/strata/__main__.py +4 -0
  7. stratactl-1.0.0/src/strata/artifacts/__init__.py +4 -0
  8. stratactl-1.0.0/src/strata/artifacts/bundle.py +111 -0
  9. stratactl-1.0.0/src/strata/artifacts/manifest.py +125 -0
  10. stratactl-1.0.0/src/strata/artifacts/source.py +60 -0
  11. stratactl-1.0.0/src/strata/aws/__init__.py +0 -0
  12. stratactl-1.0.0/src/strata/aws/session.py +34 -0
  13. stratactl-1.0.0/src/strata/bootstrap/__init__.py +29 -0
  14. stratactl-1.0.0/src/strata/bootstrap/infra/main.tf +313 -0
  15. stratactl-1.0.0/src/strata/bootstrap/infra/outputs.tf +50 -0
  16. stratactl-1.0.0/src/strata/bootstrap/infra/variables.tf +46 -0
  17. stratactl-1.0.0/src/strata/bootstrap/infra/versions.tf +16 -0
  18. stratactl-1.0.0/src/strata/cli.py +161 -0
  19. stratactl-1.0.0/src/strata/commands/__init__.py +9 -0
  20. stratactl-1.0.0/src/strata/commands/_common.py +173 -0
  21. stratactl-1.0.0/src/strata/commands/bootstrap.py +121 -0
  22. stratactl-1.0.0/src/strata/commands/dashboards.py +70 -0
  23. stratactl-1.0.0/src/strata/commands/dev.py +150 -0
  24. stratactl-1.0.0/src/strata/commands/fetch_runtime.py +51 -0
  25. stratactl-1.0.0/src/strata/commands/init.py +147 -0
  26. stratactl-1.0.0/src/strata/commands/install.py +11 -0
  27. stratactl-1.0.0/src/strata/commands/list.py +20 -0
  28. stratactl-1.0.0/src/strata/commands/logs.py +29 -0
  29. stratactl-1.0.0/src/strata/commands/platform.py +58 -0
  30. stratactl-1.0.0/src/strata/commands/uninstall.py +11 -0
  31. stratactl-1.0.0/src/strata/commands/update.py +11 -0
  32. stratactl-1.0.0/src/strata/config/__init__.py +4 -0
  33. stratactl-1.0.0/src/strata/config/defaults.toml +46 -0
  34. stratactl-1.0.0/src/strata/config/models.py +66 -0
  35. stratactl-1.0.0/src/strata/config/provider.py +228 -0
  36. stratactl-1.0.0/src/strata/dashboards/__init__.py +3 -0
  37. stratactl-1.0.0/src/strata/dashboards/sync.py +264 -0
  38. stratactl-1.0.0/src/strata/errors.py +29 -0
  39. stratactl-1.0.0/src/strata/hq/__init__.py +3 -0
  40. stratactl-1.0.0/src/strata/hq/client.py +132 -0
  41. stratactl-1.0.0/src/strata/install/__init__.py +19 -0
  42. stratactl-1.0.0/src/strata/install/modules.py +103 -0
  43. stratactl-1.0.0/src/strata/install/prompt.py +185 -0
  44. stratactl-1.0.0/src/strata/install/schema.py +132 -0
  45. stratactl-1.0.0/src/strata/install/store.py +99 -0
  46. stratactl-1.0.0/src/strata/install/tfvars.py +142 -0
  47. stratactl-1.0.0/src/strata/install/validators.py +97 -0
  48. stratactl-1.0.0/src/strata/registry/__init__.py +3 -0
  49. stratactl-1.0.0/src/strata/registry/installs.py +88 -0
  50. stratactl-1.0.0/src/strata/registry/jobs.py +115 -0
  51. stratactl-1.0.0/src/strata/runner/__init__.py +3 -0
  52. stratactl-1.0.0/src/strata/runner/__main__.py +9 -0
  53. stratactl-1.0.0/src/strata/runner/launcher.py +54 -0
  54. stratactl-1.0.0/src/strata/runner/logs.py +69 -0
  55. stratactl-1.0.0/src/strata/runner/main.py +229 -0
  56. stratactl-1.0.0/src/strata/runner/permissions.py +74 -0
  57. stratactl-1.0.0/src/strata/runner/sensitive.py +49 -0
  58. stratactl-1.0.0/src/stratactl.egg-info/PKG-INFO +119 -0
  59. stratactl-1.0.0/src/stratactl.egg-info/SOURCES.txt +63 -0
  60. stratactl-1.0.0/src/stratactl.egg-info/dependency_links.txt +1 -0
  61. stratactl-1.0.0/src/stratactl.egg-info/entry_points.txt +2 -0
  62. stratactl-1.0.0/src/stratactl.egg-info/requires.txt +11 -0
  63. stratactl-1.0.0/src/stratactl.egg-info/top_level.txt +1 -0
  64. stratactl-1.0.0/tests/test_dev_source_resolution.py +47 -0
  65. stratactl-1.0.0/tests/test_install.py +182 -0
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: stratactl
3
+ Version: 1.0.0
4
+ Summary: StrataCTL — bootstrap StrataCI into a customer AWS account and install/update/uninstall StrataBI modules via an ephemeral CodeBuild runner.
5
+ Author-email: Shaleio LLC <info@shaleio.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://shaleio.com
8
+ Project-URL: Documentation, https://shaleio.com/docs
9
+ Project-URL: Source, https://github.com/shaleio/stratactl
10
+ Project-URL: Issues, https://github.com/shaleio/stratactl/issues
11
+ Keywords: stratabi,aws,codebuild,opentofu,terraform,cli,analytics
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: System :: Installation/Setup
22
+ Classifier: Topic :: System :: Systems Administration
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: boto3>=1.34
26
+ Requires-Dist: pyyaml>=6
27
+ Requires-Dist: platformdirs>=4
28
+ Provides-Extra: runner
29
+ Requires-Dist: cryptography>=42; extra == "runner"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8; extra == "dev"
32
+ Requires-Dist: build>=1; extra == "dev"
33
+ Requires-Dist: twine>=5; extra == "dev"
34
+
35
+ # StrataCTL
36
+
37
+ The local command-line tool for operators. It installs, updates, and removes
38
+ StrataBI modules and the StrataBI platform in **your** AWS account, fetches the
39
+ account-entitled runtime, and version-controls dashboards. It runs on your machine
40
+ with your own AWS credentials — it is not a hosted service.
41
+
42
+ The same `strata` package is also the **runner program** that executes inside the
43
+ ephemeral CodeBuild runner (`python -m strata.runner`).
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ python3 -m venv ~/.venvs/strata && source ~/.venvs/strata/bin/activate
49
+ pip install -e . # from this directory; exposes the `stratactl` command
50
+ stratactl --help
51
+ ```
52
+
53
+ ## Configure
54
+
55
+ `strata init` collects a deployment's configuration into `strata.yaml` (the source
56
+ of truth) and generates the Terraform variable files — you don't hand-edit `.tfvars`.
57
+
58
+ ```bash
59
+ stratactl init # interactive; re-run to reuse answers + fill gaps
60
+ stratactl init --non-interactive # config + defaults only (automation)
61
+ ```
62
+
63
+ Settings resolve in this precedence (highest first): **CLI flag → env (`STRATA_*`)
64
+ → `./.stratactl.toml` → `~/.stratactl/config.toml` → AWS SSM (`/strataci/hq/*`) →
65
+ packaged defaults**. Point it at a customer account by pasting the
66
+ `stratactl_customer_config` output from `strataci-runner` into your config, and set
67
+ your license token:
68
+
69
+ ```bash
70
+ export STRATA_HQ_TOKEN=<your license token>
71
+ ```
72
+
73
+ AWS credentials come from your normal profile/keys (`--profile`, `AWS_PROFILE`, env).
74
+ What you can deploy is governed by your own IAM in the target account.
75
+
76
+ ## Commands
77
+
78
+ ```bash
79
+ stratactl init # configure a deployment (writes strata.yaml + tfvars)
80
+ stratactl fetch-runtime --version 1.0.0 # pull the account-entitled runtime wheel -> ./dist (verified)
81
+ stratactl install <module> # entitlement -> runner -> tofu apply, in your account
82
+ stratactl install-stratabi # install the StrataBI platform (Enterprise)
83
+ stratactl install-stratabi-dev # install the Developer/Community edition
84
+ stratactl update <module> --to <version>
85
+ stratactl uninstall <module> # runs the module's tofu destroy (always available)
86
+ stratactl list # what's installed + delivery mode
87
+ stratactl logs <job-id> # tail/replay a runner build
88
+ stratactl dashboards pull|push|list|diff # git-sync dashboards (no runner needed)
89
+ ```
90
+
91
+ `--source stratahq` (default) fetches a signed bundle from Shaleio's control plane;
92
+ `--source customer_s3` installs from your own bucket (offline / air-gapped).
93
+
94
+ ## End-to-end: deploy StrataBI Enterprise
95
+
96
+ ```bash
97
+ # 0. one-time per account: scaffold the runner (see ../strataci-runner)
98
+ # 1. build the runtime container image (see ../build_template) -> ECR image URI
99
+ # 2. configure + deploy:
100
+ export STRATA_HQ_TOKEN=<token>
101
+ stratactl init # set stratabi_image=<ECR URI>, vpc_id, subnets, allowed_ip_cidrs
102
+ stratactl install-stratabi
103
+ stratactl list
104
+ ```
105
+
106
+ Your `strata init` values ride through to the ephemeral runner automatically (the
107
+ CLI uploads them; the runner applies them over the bundle's baked defaults), so the
108
+ platform deploys with your networking/image without any manual `.tfvars` on the runner.
109
+
110
+ ## Layout
111
+
112
+ | Path | What |
113
+ |------|------|
114
+ | `src/strata/commands/` | CLI subcommands (`init`, `install`, `fetch_runtime`, `dashboards`, …) |
115
+ | `src/strata/hq/` | StrataHQ client (SigV4-signed entitlement calls) |
116
+ | `src/strata/runner/` | the program that runs inside CodeBuild |
117
+ | `src/strata/install/` | the `strata init` wizard (schema / prompt / store / tfvars) |
118
+ | `src/strata/artifacts/` | bundle download / verify / manifest |
119
+ | `tests/` | unit tests (`pytest`) |
@@ -0,0 +1,85 @@
1
+ # StrataCTL
2
+
3
+ The local command-line tool for operators. It installs, updates, and removes
4
+ StrataBI modules and the StrataBI platform in **your** AWS account, fetches the
5
+ account-entitled runtime, and version-controls dashboards. It runs on your machine
6
+ with your own AWS credentials — it is not a hosted service.
7
+
8
+ The same `strata` package is also the **runner program** that executes inside the
9
+ ephemeral CodeBuild runner (`python -m strata.runner`).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ python3 -m venv ~/.venvs/strata && source ~/.venvs/strata/bin/activate
15
+ pip install -e . # from this directory; exposes the `stratactl` command
16
+ stratactl --help
17
+ ```
18
+
19
+ ## Configure
20
+
21
+ `strata init` collects a deployment's configuration into `strata.yaml` (the source
22
+ of truth) and generates the Terraform variable files — you don't hand-edit `.tfvars`.
23
+
24
+ ```bash
25
+ stratactl init # interactive; re-run to reuse answers + fill gaps
26
+ stratactl init --non-interactive # config + defaults only (automation)
27
+ ```
28
+
29
+ Settings resolve in this precedence (highest first): **CLI flag → env (`STRATA_*`)
30
+ → `./.stratactl.toml` → `~/.stratactl/config.toml` → AWS SSM (`/strataci/hq/*`) →
31
+ packaged defaults**. Point it at a customer account by pasting the
32
+ `stratactl_customer_config` output from `strataci-runner` into your config, and set
33
+ your license token:
34
+
35
+ ```bash
36
+ export STRATA_HQ_TOKEN=<your license token>
37
+ ```
38
+
39
+ AWS credentials come from your normal profile/keys (`--profile`, `AWS_PROFILE`, env).
40
+ What you can deploy is governed by your own IAM in the target account.
41
+
42
+ ## Commands
43
+
44
+ ```bash
45
+ stratactl init # configure a deployment (writes strata.yaml + tfvars)
46
+ stratactl fetch-runtime --version 1.0.0 # pull the account-entitled runtime wheel -> ./dist (verified)
47
+ stratactl install <module> # entitlement -> runner -> tofu apply, in your account
48
+ stratactl install-stratabi # install the StrataBI platform (Enterprise)
49
+ stratactl install-stratabi-dev # install the Developer/Community edition
50
+ stratactl update <module> --to <version>
51
+ stratactl uninstall <module> # runs the module's tofu destroy (always available)
52
+ stratactl list # what's installed + delivery mode
53
+ stratactl logs <job-id> # tail/replay a runner build
54
+ stratactl dashboards pull|push|list|diff # git-sync dashboards (no runner needed)
55
+ ```
56
+
57
+ `--source stratahq` (default) fetches a signed bundle from Shaleio's control plane;
58
+ `--source customer_s3` installs from your own bucket (offline / air-gapped).
59
+
60
+ ## End-to-end: deploy StrataBI Enterprise
61
+
62
+ ```bash
63
+ # 0. one-time per account: scaffold the runner (see ../strataci-runner)
64
+ # 1. build the runtime container image (see ../build_template) -> ECR image URI
65
+ # 2. configure + deploy:
66
+ export STRATA_HQ_TOKEN=<token>
67
+ stratactl init # set stratabi_image=<ECR URI>, vpc_id, subnets, allowed_ip_cidrs
68
+ stratactl install-stratabi
69
+ stratactl list
70
+ ```
71
+
72
+ Your `strata init` values ride through to the ephemeral runner automatically (the
73
+ CLI uploads them; the runner applies them over the bundle's baked defaults), so the
74
+ platform deploys with your networking/image without any manual `.tfvars` on the runner.
75
+
76
+ ## Layout
77
+
78
+ | Path | What |
79
+ |------|------|
80
+ | `src/strata/commands/` | CLI subcommands (`init`, `install`, `fetch_runtime`, `dashboards`, …) |
81
+ | `src/strata/hq/` | StrataHQ client (SigV4-signed entitlement calls) |
82
+ | `src/strata/runner/` | the program that runs inside CodeBuild |
83
+ | `src/strata/install/` | the `strata init` wizard (schema / prompt / store / tfvars) |
84
+ | `src/strata/artifacts/` | bundle download / verify / manifest |
85
+ | `tests/` | unit tests (`pytest`) |
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ # PyPI distribution name (intended; verify availability before first publish).
7
+ name = "stratactl"
8
+ # Versioning is Major.Sprint.Patch (Shaleio ecosystem sprint clock), not SemVer.
9
+ # Sprint 0 begins 2026-07-27. See "Versioning" in the README.
10
+ version = "1.0.0"
11
+ description = "StrataCTL — bootstrap StrataCI into a customer AWS account and install/update/uninstall StrataBI modules via an ephemeral CodeBuild runner."
12
+ readme = "README.md"
13
+ requires-python = ">=3.11"
14
+ license = { text = "Apache-2.0" }
15
+ authors = [{ name = "Shaleio LLC", email = "info@shaleio.com" }]
16
+ keywords = ["stratabi", "aws", "codebuild", "opentofu", "terraform", "cli", "analytics"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "Intended Audience :: System Administrators",
22
+ "License :: OSI Approved :: Apache Software License",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: System :: Installation/Setup",
28
+ "Topic :: System :: Systems Administration",
29
+ ]
30
+ dependencies = [
31
+ "boto3>=1.34",
32
+ "pyyaml>=6",
33
+ "platformdirs>=4",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ # Installed in the runner image/buildspec — needed for offline bundle-signature
38
+ # verification (Decision C). The CLI itself does not require it.
39
+ runner = ["cryptography>=42"]
40
+ # Development / test tooling.
41
+ dev = [
42
+ "pytest>=8",
43
+ "build>=1",
44
+ "twine>=5",
45
+ ]
46
+
47
+ [project.urls]
48
+ Homepage = "https://shaleio.com"
49
+ Documentation = "https://shaleio.com/docs"
50
+ Source = "https://github.com/shaleio/stratactl"
51
+ Issues = "https://github.com/shaleio/stratactl/issues"
52
+
53
+ [project.scripts]
54
+ stratactl = "strata.cli:main"
55
+
56
+ [tool.setuptools.packages.find]
57
+ where = ["src"]
58
+
59
+ [tool.setuptools.package-data]
60
+ # Ship every non-.py asset the CLI + bootstrap need at runtime: config defaults,
61
+ # bundled OpenTofu templates, install schemas, and the packaged StrataCI bootstrap
62
+ # infrastructure (see src/strata/bootstrap/infra/ — versioned, immutable package data).
63
+ strata = [
64
+ "config/*.toml",
65
+ "bootstrap/**/*",
66
+ "install/**/*.json",
67
+ "**/*.tf",
68
+ "**/*.tftpl",
69
+ "**/*.json",
70
+ ]
71
+
72
+ [tool.pytest.ini_options]
73
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """StrataCTL / StrataCI shared package.
2
+
3
+ One codebase, two entry points:
4
+ * `strata.cli` — the local operator CLI (`stratactl ...`)
5
+ * `strata.runner` — the program that runs inside the ephemeral CodeBuild runner
6
+ """
7
+
8
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from strata.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ from strata.artifacts.manifest import Manifest, load_and_validate
2
+ from strata.artifacts.source import SourceSpec, resolve_source
3
+
4
+ __all__ = ["Manifest", "load_and_validate", "SourceSpec", "resolve_source"]
@@ -0,0 +1,111 @@
1
+ """Artifact bundle handling, used by the runner: download, verify, safe-extract.
2
+
3
+ Bundle layout:
4
+ module.zip
5
+ ├── stratabi-module.json
6
+ ├── tofu/{main,variables,outputs,versions}.tf
7
+ ├── config/defaults.json
8
+ └── README.md
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import urllib.request
15
+ import zipfile
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ from strata.errors import ArtifactError
20
+
21
+ MAX_BUNDLE_BYTES = 256 * 1024 * 1024 # 256 MB cap
22
+ MAX_FILES = 5000
23
+
24
+
25
+ def download_url(url: str, dest: Path, timeout: int = 60) -> Path:
26
+ """Download a presigned (or any) URL to dest. Never logs the URL."""
27
+ try:
28
+ with urllib.request.urlopen(url, timeout=timeout) as resp, open(dest, "wb") as fh:
29
+ total = 0
30
+ while chunk := resp.read(1024 * 256):
31
+ total += len(chunk)
32
+ if total > MAX_BUNDLE_BYTES:
33
+ raise ArtifactError("bundle exceeds size cap")
34
+ fh.write(chunk)
35
+ except OSError as exc:
36
+ raise ArtifactError(f"failed to download artifact: {exc}") from exc
37
+ return dest
38
+
39
+
40
+ def download_s3(s3_client, s3_uri: str, dest: Path) -> Path:
41
+ if not s3_uri.startswith("s3://"):
42
+ raise ArtifactError(f"not an s3 uri: {s3_uri}")
43
+ bucket, key = s3_uri[len("s3://"):].split("/", 1)
44
+ try:
45
+ s3_client.download_file(bucket, key, str(dest))
46
+ except Exception as exc:
47
+ raise ArtifactError(f"failed to download {s3_uri}: {exc}") from exc
48
+ return dest
49
+
50
+
51
+ def verify_sha256(path: Path, expected: Optional[str]) -> str:
52
+ h = hashlib.sha256()
53
+ with open(path, "rb") as fh:
54
+ while chunk := fh.read(1024 * 256):
55
+ h.update(chunk)
56
+ actual = h.hexdigest()
57
+ if expected and actual.lower() != expected.lower():
58
+ raise ArtifactError(
59
+ f"artifact checksum mismatch (expected {expected[:12]}…, got {actual[:12]}…)"
60
+ )
61
+ return actual
62
+
63
+
64
+ def safe_extract(zip_path: Path, workdir: Path) -> Path:
65
+ """Extract a zip, rejecting absolute paths / traversal / overlarge bundles."""
66
+ workdir.mkdir(parents=True, exist_ok=True)
67
+ try:
68
+ with zipfile.ZipFile(zip_path) as zf:
69
+ members = zf.namelist()
70
+ if len(members) > MAX_FILES:
71
+ raise ArtifactError("bundle has too many entries")
72
+ for name in members:
73
+ target = (workdir / name).resolve()
74
+ if not str(target).startswith(str(workdir.resolve())):
75
+ raise ArtifactError(f"unsafe path in bundle: {name}")
76
+ zf.extractall(workdir)
77
+ except zipfile.BadZipFile as exc:
78
+ raise ArtifactError(f"corrupt bundle: {exc}") from exc
79
+ return workdir
80
+
81
+
82
+ def verify_signature(digest_hex: str, signature_b64: str, public_key_b64der: str) -> None:
83
+ """Offline verify a KMS ECDSA-P256 signature over the bundle's sha256 digest
84
+ (Decision C). No AWS call, no cross-account KMS. Raises ArtifactError on
85
+ failure. `cryptography` is imported lazily — only needed when verifying."""
86
+ import base64
87
+
88
+ try:
89
+ from cryptography.hazmat.primitives import hashes
90
+ from cryptography.hazmat.primitives.asymmetric import ec, utils
91
+ from cryptography.hazmat.primitives.serialization import load_der_public_key
92
+ except ModuleNotFoundError as exc: # pragma: no cover
93
+ raise ArtifactError("signature verification requires the 'cryptography' package") from exc
94
+
95
+ try:
96
+ pub = load_der_public_key(base64.b64decode(public_key_b64der))
97
+ pub.verify(
98
+ base64.b64decode(signature_b64),
99
+ bytes.fromhex(digest_hex),
100
+ ec.ECDSA(utils.Prehashed(hashes.SHA256())),
101
+ )
102
+ except Exception as exc: # invalid signature or key
103
+ raise ArtifactError(f"bundle signature verification failed: {exc}") from exc
104
+
105
+
106
+ def cache_to_customer_s3(s3_client, bucket: str, module_id: str, version: str,
107
+ zip_path: Path) -> str:
108
+ """Store the verified bundle so uninstall/update work without StrataHQ."""
109
+ key = f"artifacts/{module_id}/{version}/module.zip"
110
+ s3_client.upload_file(str(zip_path), bucket, key)
111
+ return f"s3://{bucket}/{key}"
@@ -0,0 +1,125 @@
1
+ """stratabi-module.json parsing + validation.
2
+
3
+ `required_permissions` drives least-privilege deploys (Decision A): the runner
4
+ builds an IAM session policy from it and assumes a deploy role scoped DOWN to
5
+ exactly what the module declares. Two accepted forms:
6
+
7
+ Structured (preferred — enables tight scoping; module authors MUST use this for
8
+ third-party modules):
9
+ "required_permissions": [
10
+ {"service": "s3",
11
+ "actions": ["s3:CreateBucket", "s3:PutObject", "s3:DeleteObject"],
12
+ "resources": ["arn:aws:s3:::acme-iceberg-*", "arn:aws:s3:::acme-iceberg-*/*"]}
13
+ ]
14
+
15
+ Simple (legacy / Shaleio-internal only — results in a BROAD service:* grant):
16
+ "required_permissions": ["s3", "lambda", "dynamodb"]
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from strata.errors import ManifestError
27
+
28
+ VALID_ENTRYPOINTS = {"tofu"}
29
+
30
+ # Delivery mode governs whether the tofu bundle is persisted into the customer's
31
+ # environment. "local": the runner caches the verified bundle to the customer
32
+ # bucket, so teardown works offline/airgapped and without HQ. "managed": the
33
+ # bundle is never persisted; uninstall re-fetches it from HQ (destroy is always
34
+ # permitted, even for a lapsed/retired grant). Default local.
35
+ VALID_DELIVERY = {"local", "managed"}
36
+
37
+
38
+ @dataclass
39
+ class Manifest:
40
+ module_id: str
41
+ version: str
42
+ entrypoint: str
43
+ state_key: str
44
+ delivery: str = "local"
45
+ required_permissions: list[Any] = field(default_factory=list)
46
+ supports_destroy: bool = False
47
+ raw: dict[str, Any] = field(default_factory=dict)
48
+
49
+ @property
50
+ def permissions_are_scoped(self) -> bool:
51
+ """True when every permission entry is structured (service/actions/resources)."""
52
+ if not self.required_permissions:
53
+ return False
54
+ return all(isinstance(p, dict) for p in self.required_permissions)
55
+
56
+
57
+ def load_and_validate(path: Path) -> Manifest:
58
+ if not path.is_file():
59
+ raise ManifestError(f"manifest not found: {path}")
60
+ try:
61
+ data = json.loads(path.read_text("utf-8"))
62
+ except json.JSONDecodeError as exc:
63
+ raise ManifestError(f"manifest is not valid JSON: {exc}") from exc
64
+ return validate(data)
65
+
66
+
67
+ def validate(data: dict[str, Any]) -> Manifest:
68
+ for required in ("module_id", "version", "entrypoint", "state_key"):
69
+ if not data.get(required):
70
+ raise ManifestError(f"manifest missing required field '{required}'")
71
+
72
+ entrypoint = data["entrypoint"]
73
+ if entrypoint not in VALID_ENTRYPOINTS:
74
+ raise ManifestError(
75
+ f"unsupported entrypoint '{entrypoint}' (expected one of {sorted(VALID_ENTRYPOINTS)})"
76
+ )
77
+
78
+ state_key = data["state_key"]
79
+ if state_key.startswith("/") or ".." in state_key:
80
+ raise ManifestError(f"unsafe state_key '{state_key}'")
81
+
82
+ delivery = data.get("delivery", "local")
83
+ if delivery not in VALID_DELIVERY:
84
+ raise ManifestError(
85
+ f"delivery must be one of {sorted(VALID_DELIVERY)}, got '{delivery}'"
86
+ )
87
+
88
+ perms = data.get("required_permissions", [])
89
+ if not isinstance(perms, list):
90
+ raise ManifestError("required_permissions must be a list")
91
+ for entry in perms:
92
+ _validate_perm_entry(entry)
93
+
94
+ return Manifest(
95
+ module_id=data["module_id"],
96
+ version=str(data["version"]),
97
+ entrypoint=entrypoint,
98
+ state_key=state_key,
99
+ delivery=delivery,
100
+ required_permissions=perms,
101
+ supports_destroy=bool(data.get("supports_destroy", False)),
102
+ raw=data,
103
+ )
104
+
105
+
106
+ def _validate_perm_entry(entry: Any) -> None:
107
+ if isinstance(entry, str):
108
+ if not entry:
109
+ raise ManifestError("empty permission entry")
110
+ return
111
+ if isinstance(entry, dict):
112
+ if not entry.get("service"):
113
+ raise ManifestError("structured permission entry missing 'service'")
114
+ actions = entry.get("actions")
115
+ if not isinstance(actions, list) or not actions:
116
+ raise ManifestError(
117
+ f"permission entry for '{entry.get('service')}' needs a non-empty 'actions' list"
118
+ )
119
+ resources = entry.get("resources", ["*"])
120
+ if not isinstance(resources, list) or not resources:
121
+ raise ManifestError(
122
+ f"permission entry for '{entry.get('service')}' needs a non-empty 'resources' list"
123
+ )
124
+ return
125
+ raise ManifestError(f"invalid permission entry: {entry!r}")
@@ -0,0 +1,60 @@
1
+ """Resolve where a module artifact comes from.
2
+
3
+ A module spec can be:
4
+ * "iceberg-explorer" -> use the default source from config
5
+ * "stratahq/iceberg-explorer" -> force StrataHQ (entitlement required)
6
+ * "customer_s3/iceberg-explorer"-> force customer S3
7
+ * "s3://bucket/key/module.zip" -> explicit customer S3 object
8
+ * "https://.../module.zip" -> explicit PUBLIC artifact (no StrataHQ contact,
9
+ no entitlement) — e.g. a GitHub release of the
10
+ source-available Developer / Community edition.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from typing import Optional
17
+
18
+ from strata.config.models import Settings
19
+
20
+ KIND_STRATAHQ = "stratahq"
21
+ KIND_CUSTOMER = "customer_s3"
22
+ KIND_PUBLIC = "public"
23
+
24
+
25
+ @dataclass
26
+ class SourceSpec:
27
+ kind: str # "stratahq" | "customer_s3" | "public"
28
+ module_id: str
29
+ customer_s3_uri: Optional[str] = None # set when kind == customer_s3 and explicit
30
+ public_url: Optional[str] = None # set when kind == public (http/https)
31
+ sha256: Optional[str] = None
32
+
33
+
34
+ def _norm_module_id(name: str) -> str:
35
+ return name.replace("-", "_")
36
+
37
+
38
+ def resolve_source(module_spec: str, settings: Settings,
39
+ cli_source: Optional[str] = None) -> SourceSpec:
40
+ # Explicit http(s) URL is a PUBLIC artifact — fetched directly, no StrataHQ,
41
+ # no entitlement (used by the source-available dev/community edition).
42
+ if module_spec.startswith(("http://", "https://")):
43
+ tail = module_spec.split("/")[-1].split("?")[0]
44
+ module_id = _norm_module_id(tail.removesuffix(".zip"))
45
+ return SourceSpec(kind=KIND_PUBLIC, module_id=module_id, public_url=module_spec)
46
+
47
+ # Explicit s3:// URI is unambiguously a customer artifact.
48
+ if module_spec.startswith("s3://"):
49
+ tail = module_spec.split("/")[-1]
50
+ module_id = _norm_module_id(tail.removesuffix(".zip"))
51
+ return SourceSpec(kind=KIND_CUSTOMER, module_id=module_id, customer_s3_uri=module_spec)
52
+
53
+ kind = cli_source or settings.source
54
+ module_name = module_spec
55
+ if "/" in module_spec:
56
+ prefix, module_name = module_spec.split("/", 1)
57
+ if prefix in (KIND_STRATAHQ, KIND_CUSTOMER, KIND_PUBLIC):
58
+ kind = prefix
59
+
60
+ return SourceSpec(kind=kind, module_id=_norm_module_id(module_name))
File without changes
@@ -0,0 +1,34 @@
1
+ """boto3 session/clients for the TARGET (customer) account.
2
+
3
+ The operator's own AWS credentials drive everything here — Shaleio never holds
4
+ customer-account credentials.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from functools import lru_cache
10
+ from typing import Optional
11
+
12
+ import boto3
13
+
14
+ from strata.config.models import Settings
15
+
16
+
17
+ def make_session(settings: Settings) -> boto3.Session:
18
+ return boto3.Session(profile_name=settings.profile, region_name=settings.region)
19
+
20
+
21
+ @lru_cache(maxsize=1)
22
+ def account_id(session: boto3.Session) -> str:
23
+ return session.client("sts").get_caller_identity()["Account"]
24
+
25
+
26
+ def client(session: boto3.Session, service: str):
27
+ return session.client(service)
28
+
29
+
30
+ def caller_arn(session: boto3.Session) -> Optional[str]:
31
+ try:
32
+ return session.client("sts").get_caller_identity()["Arn"]
33
+ except Exception:
34
+ return None
@@ -0,0 +1,29 @@
1
+ """Packaged StrataCI bootstrap infrastructure (immutable, version-matched).
2
+
3
+ The OpenTofu that stands StrataCI up inside a customer account ships as package
4
+ data under ``strata/bootstrap/infra/`` so a pipx-installed ``stratactl`` is fully
5
+ self-contained (no repo checkout needed). ``bootstrap_infra_files()`` returns the
6
+ packaged ``.tf`` files; the ``bootstrap`` command copies them into a per-account
7
+ working directory before running tofu — the packaged copy is never mutated.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from importlib import resources
13
+ from importlib.abc import Traversable
14
+
15
+ __all__ = ["bootstrap_infra_root", "bootstrap_infra_files"]
16
+
17
+
18
+ def bootstrap_infra_root() -> Traversable:
19
+ """The packaged bootstrap infra directory as an importlib.resources Traversable."""
20
+ return resources.files(__package__).joinpath("infra")
21
+
22
+
23
+ def bootstrap_infra_files() -> list[str]:
24
+ """Names of the packaged infra files (Terraform/OpenTofu sources + lock)."""
25
+ root = bootstrap_infra_root()
26
+ return sorted(
27
+ p.name for p in root.iterdir()
28
+ if p.name.endswith((".tf", ".tftpl", ".hcl")) or p.name.endswith(".example")
29
+ )