plinth-cli 0.2.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 (64) hide show
  1. plinth_cli-0.2.0/.gitignore +8 -0
  2. plinth_cli-0.2.0/LICENSE +21 -0
  3. plinth_cli-0.2.0/PKG-INFO +174 -0
  4. plinth_cli-0.2.0/README.md +150 -0
  5. plinth_cli-0.2.0/examples/hybrid-monorepo/plinth.yaml +12 -0
  6. plinth_cli-0.2.0/examples/hybrid-split-repo/plinth.yaml +12 -0
  7. plinth_cli-0.2.0/examples/microservices-monorepo/plinth.yaml +12 -0
  8. plinth_cli-0.2.0/examples/microservices-split-repo/plinth.yaml +12 -0
  9. plinth_cli-0.2.0/examples/modular-monolith-monorepo/plinth.yaml +10 -0
  10. plinth_cli-0.2.0/examples/modular-monolith-split-repo/plinth.yaml +10 -0
  11. plinth_cli-0.2.0/plinth/__init__.py +1 -0
  12. plinth_cli-0.2.0/plinth/__main__.py +5 -0
  13. plinth_cli-0.2.0/plinth/add.py +97 -0
  14. plinth_cli-0.2.0/plinth/blueprints/ci/github/dot-github/workflows/ci.yaml.j2 +38 -0
  15. plinth_cli-0.2.0/plinth/blueprints/ci/gitlab/dot-gitlab-ci.yml.j2 +36 -0
  16. plinth_cli-0.2.0/plinth/blueprints/gitops/applicationset.yaml.j2 +46 -0
  17. plinth_cli-0.2.0/plinth/blueprints/gitops/values.yaml.j2 +2 -0
  18. plinth_cli-0.2.0/plinth/blueprints/repo/app/README.md.j2 +63 -0
  19. plinth_cli-0.2.0/plinth/blueprints/repo/app/dagger.json.j2 +8 -0
  20. plinth_cli-0.2.0/plinth/blueprints/repo/app/dot-dagger/dot-gitignore +3 -0
  21. plinth_cli-0.2.0/plinth/blueprints/repo/app/dot-dagger/pyproject.toml +12 -0
  22. plinth_cli-0.2.0/plinth/blueprints/repo/app/dot-dagger/src/ci/__init__.py +3 -0
  23. plinth_cli-0.2.0/plinth/blueprints/repo/app/dot-dagger/src/ci/main.py.j2 +133 -0
  24. plinth_cli-0.2.0/plinth/blueprints/repo/app/dot-gitignore +4 -0
  25. plinth_cli-0.2.0/plinth/blueprints/repo/gitops/README.md.j2 +53 -0
  26. plinth_cli-0.2.0/plinth/blueprints/repo/gitops/apps/project.yaml.j2 +15 -0
  27. plinth_cli-0.2.0/plinth/blueprints/repo/gitops/bootstrap/root.yaml.j2 +20 -0
  28. plinth_cli-0.2.0/plinth/blueprints/repo/gitops/charts/workload/Chart.yaml +5 -0
  29. plinth_cli-0.2.0/plinth/blueprints/repo/gitops/charts/workload/templates/_helpers.tpl +13 -0
  30. plinth_cli-0.2.0/plinth/blueprints/repo/gitops/charts/workload/templates/deployment.yaml +61 -0
  31. plinth_cli-0.2.0/plinth/blueprints/repo/gitops/charts/workload/templates/service.yaml +20 -0
  32. plinth_cli-0.2.0/plinth/blueprints/repo/gitops/charts/workload/values.yaml +9 -0
  33. plinth_cli-0.2.0/plinth/blueprints/repo/gitops/scripts/bootstrap-local.sh.j2 +23 -0
  34. plinth_cli-0.2.0/plinth/blueprints/workload/common/Dockerfile.j2 +14 -0
  35. plinth_cli-0.2.0/plinth/blueprints/workload/common/app/__init__.py +0 -0
  36. plinth_cli-0.2.0/plinth/blueprints/workload/common/dot-dockerignore +4 -0
  37. plinth_cli-0.2.0/plinth/blueprints/workload/common/score.yaml.j2 +33 -0
  38. plinth_cli-0.2.0/plinth/blueprints/workload/common/tests/__init__.py +0 -0
  39. plinth_cli-0.2.0/plinth/blueprints/workload/modular-monolith/app/main.py.j2 +63 -0
  40. plinth_cli-0.2.0/plinth/blueprints/workload/modular-monolith/app/modules/__init__.py +5 -0
  41. plinth_cli-0.2.0/plinth/blueprints/workload/modular-monolith/app/modules/example.py +12 -0
  42. plinth_cli-0.2.0/plinth/blueprints/workload/modular-monolith/tests/test_main.py.j2 +42 -0
  43. plinth_cli-0.2.0/plinth/blueprints/workload/service/app/main.py.j2 +54 -0
  44. plinth_cli-0.2.0/plinth/blueprints/workload/service/tests/test_main.py.j2 +41 -0
  45. plinth_cli-0.2.0/plinth/blueprints/workload/worker/app/main.py.j2 +44 -0
  46. plinth_cli-0.2.0/plinth/blueprints/workload/worker/tests/test_main.py.j2 +26 -0
  47. plinth_cli-0.2.0/plinth/cli.py +185 -0
  48. plinth_cli-0.2.0/plinth/config.py +51 -0
  49. plinth_cli-0.2.0/plinth/loader.py +49 -0
  50. plinth_cli-0.2.0/plinth/plan.py +70 -0
  51. plinth_cli-0.2.0/plinth/render.py +119 -0
  52. plinth_cli-0.2.0/plinth/schema.py +158 -0
  53. plinth_cli-0.2.0/plinth/wizard.py +129 -0
  54. plinth_cli-0.2.0/plinth/write.py +72 -0
  55. plinth_cli-0.2.0/pyproject.toml +56 -0
  56. plinth_cli-0.2.0/tests/__init__.py +0 -0
  57. plinth_cli-0.2.0/tests/conftest.py +48 -0
  58. plinth_cli-0.2.0/tests/test_add.py +92 -0
  59. plinth_cli-0.2.0/tests/test_cli.py +97 -0
  60. plinth_cli-0.2.0/tests/test_generate.py +169 -0
  61. plinth_cli-0.2.0/tests/test_plan.py +52 -0
  62. plinth_cli-0.2.0/tests/test_schema.py +134 -0
  63. plinth_cli-0.2.0/tests/test_wizard.py +73 -0
  64. plinth_cli-0.2.0/tests/test_write.py +58 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ generated/
7
+ dist/
8
+ brag-output*/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dfoshidero
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.5
2
+ Name: plinth-cli
3
+ Version: 0.2.0
4
+ Summary: Generates local, ready-to-push application and GitOps repositories from one config.
5
+ Project-URL: Homepage, https://github.com/dfoshidero/plinth
6
+ Project-URL: Issues, https://github.com/dfoshidero/plinth/issues
7
+ Author: dfoshidero
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: argo-cd,dagger,generator,gitops,monorepo,scaffolding,score
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Code Generators
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: jinja2<4,>=3.1
20
+ Requires-Dist: pydantic<3,>=2.11
21
+ Requires-Dist: pyyaml<7,>=6.0
22
+ Requires-Dist: questionary<3,>=2.1
23
+ Description-Content-Type: text/markdown
24
+
25
+ # plinth
26
+
27
+ Generate ready-to-push application and GitOps repositories from one config file.
28
+
29
+ plinth turns a short `plinth.yaml` into git repositories with working application code, a
30
+ [Dagger](https://dagger.io) CI pipeline, [Score](https://score.dev) workload specs, and an
31
+ [Argo CD](https://argo-cd.readthedocs.io) GitOps repository that deploys them. Start as a
32
+ modular monolith, a set of microservices or a hybrid of both, in one repo or one repo per app,
33
+ and grow later with `plinth add`.
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ uv tool install plinth-cli # or: pipx install plinth-cli
39
+ plinth --version
40
+ ```
41
+
42
+ The PyPI package is `plinth-cli`; the command is `plinth`. Requires Python 3.12+.
43
+
44
+ ## Quick start
45
+
46
+ Answer a few questions:
47
+
48
+ ```sh
49
+ plinth project
50
+ ```
51
+
52
+ or write a config and generate from it:
53
+
54
+ ```yaml
55
+ # plinth.yaml
56
+ project:
57
+ name: atlas
58
+ architecture: hybrid # modular-monolith | microservices | hybrid
59
+ layout: monorepo # monorepo | split-repo
60
+ gitProvider: github # github | gitlab | generic
61
+ gitBaseUrl: https://github.com/acme
62
+ registry: ghcr.io/acme
63
+ applications:
64
+ - {name: api, kind: modular-monolith}
65
+ - {name: worker, kind: worker}
66
+ - {name: notifier, kind: service, port: 8081}
67
+ environments: [dev, production]
68
+ ```
69
+
70
+ ```sh
71
+ plinth validate --config plinth.yaml
72
+ plinth generate --config plinth.yaml --output .
73
+ ```
74
+
75
+ This writes `./atlas/` containing one folder per repository, each initialised with git, an
76
+ initial commit and an `origin` remote under `gitBaseUrl`. Create the empty repositories on your
77
+ git host and push.
78
+
79
+ More configs live in [`examples/`](examples/): every architecture with both layouts.
80
+
81
+ ## What you get
82
+
83
+ **Application repositories** (one for a monorepo, one per app for split-repo):
84
+
85
+ - a small, dependency-free Python app per workload with a `/health` endpoint (services and the
86
+ monolith) or a run loop (workers), plus tests; replace it with your own code
87
+ - a `Dockerfile` and a `score.yaml` describing how the workload runs
88
+ - a Dagger module (`.dagger/`) with `test`, `build` and `ship` functions that run identically on
89
+ your laptop and in CI
90
+ - a CI file for your provider (`.github/workflows/ci.yaml` or `.gitlab-ci.yml`) that tests pull
91
+ requests and ships `main`
92
+
93
+ **A GitOps repository:**
94
+
95
+ - an Argo CD app-of-apps (`bootstrap/root.yaml`) and one ApplicationSet per environment
96
+ - a Helm chart that deploys any workload straight from its `score.yaml`
97
+ - per-environment values, and `release.yaml` files written by CI on every ship
98
+ - `scripts/bootstrap-local.sh` to run everything on a local [k3d](https://k3d.io) cluster
99
+
100
+ The flow: push to `main` → CI tests, builds and pushes images tagged with the commit → CI writes
101
+ `environments/dev/<workload>/release.yaml` → Argo CD deploys it. Promote by copying a
102
+ `release.yaml` to another environment.
103
+
104
+ ## Grow a project
105
+
106
+ Add a service or worker to a generated project:
107
+
108
+ ```sh
109
+ cd atlas
110
+ plinth add service billing --port 8090
111
+ plinth add worker mailer
112
+ ```
113
+
114
+ plinth creates the new workload's files (and a new repository for split-repo layouts) and
115
+ updates `plinth.yaml`. Adding to a modular monolith turns it into a hybrid. Existing files are
116
+ never overwritten; any that plinth would now generate differently are listed for you to review.
117
+
118
+ ## Commands
119
+
120
+ | Command | What it does |
121
+ |---|---|
122
+ | `plinth project [--output DIR]` | interactive questions, then generate |
123
+ | `plinth validate --config FILE` | check a config and show the repositories it produces |
124
+ | `plinth generate --config FILE --output DIR [--dry-run]` | write the repositories |
125
+ | `plinth add {service,worker} NAME [--port N]` | add a workload to an existing project |
126
+
127
+ `--no-git` skips `git init` and the initial commit.
128
+
129
+ ## Configuration
130
+
131
+ | Key | Notes |
132
+ |---|---|
133
+ | `project.name` | kebab-case, 2-30 chars; prefixes every repository name |
134
+ | `project.architecture` | `modular-monolith` (one monolith), `microservices` (services/workers only), `hybrid` (one monolith plus services/workers) |
135
+ | `project.layout` | `monorepo` (`<name>-app` holds every workload) or `split-repo` (`<name>-<app>` each) |
136
+ | `project.gitProvider` | picks the CI file; `generic` generates none |
137
+ | `project.gitBaseUrl` | `https://host/org` the repositories live under |
138
+ | `project.registry` | `host/path` images are pushed to |
139
+ | `applications[]` | `name`, `kind` (`modular-monolith`, `service`, `worker`), optional `port` for HTTP kinds |
140
+ | `environments` | must include `dev`, which CI releases to |
141
+
142
+ Defaults and pinned tool versions are in [`plinth/config.py`](plinth/config.py).
143
+
144
+ ## When not to use plinth
145
+
146
+ - You only need a single repository from a template: [Copier](https://copier.readthedocs.io)
147
+ or [Cookiecutter](https://cookiecutter.readthedocs.io) are simpler, and Copier can update
148
+ projects later.
149
+ - You already run [Backstage](https://backstage.io): its software templates fit better there.
150
+ - You don't deploy to Kubernetes: the GitOps half won't help you.
151
+
152
+ plinth generates once. It does not keep generated repositories in sync with newer templates;
153
+ after generation the code is yours.
154
+
155
+ ## Development
156
+
157
+ ```sh
158
+ uv sync
159
+ uv run pytest
160
+ uv run ruff check . && uv run ruff format --check .
161
+ ```
162
+
163
+ Templates live in [`plinth/blueprints/`](plinth/blueprints/). Files ending in `.j2` are rendered
164
+ with Jinja; path segments starting with `dot-` are written with a leading `.`.
165
+
166
+ ## Contributing
167
+
168
+ Contributions are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) first; in short, open an
169
+ issue before large changes, and every contribution must be written by a human (no AI-generated
170
+ code or text).
171
+
172
+ ## License
173
+
174
+ [MIT](LICENSE)
@@ -0,0 +1,150 @@
1
+ # plinth
2
+
3
+ Generate ready-to-push application and GitOps repositories from one config file.
4
+
5
+ plinth turns a short `plinth.yaml` into git repositories with working application code, a
6
+ [Dagger](https://dagger.io) CI pipeline, [Score](https://score.dev) workload specs, and an
7
+ [Argo CD](https://argo-cd.readthedocs.io) GitOps repository that deploys them. Start as a
8
+ modular monolith, a set of microservices or a hybrid of both, in one repo or one repo per app,
9
+ and grow later with `plinth add`.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ uv tool install plinth-cli # or: pipx install plinth-cli
15
+ plinth --version
16
+ ```
17
+
18
+ The PyPI package is `plinth-cli`; the command is `plinth`. Requires Python 3.12+.
19
+
20
+ ## Quick start
21
+
22
+ Answer a few questions:
23
+
24
+ ```sh
25
+ plinth project
26
+ ```
27
+
28
+ or write a config and generate from it:
29
+
30
+ ```yaml
31
+ # plinth.yaml
32
+ project:
33
+ name: atlas
34
+ architecture: hybrid # modular-monolith | microservices | hybrid
35
+ layout: monorepo # monorepo | split-repo
36
+ gitProvider: github # github | gitlab | generic
37
+ gitBaseUrl: https://github.com/acme
38
+ registry: ghcr.io/acme
39
+ applications:
40
+ - {name: api, kind: modular-monolith}
41
+ - {name: worker, kind: worker}
42
+ - {name: notifier, kind: service, port: 8081}
43
+ environments: [dev, production]
44
+ ```
45
+
46
+ ```sh
47
+ plinth validate --config plinth.yaml
48
+ plinth generate --config plinth.yaml --output .
49
+ ```
50
+
51
+ This writes `./atlas/` containing one folder per repository, each initialised with git, an
52
+ initial commit and an `origin` remote under `gitBaseUrl`. Create the empty repositories on your
53
+ git host and push.
54
+
55
+ More configs live in [`examples/`](examples/): every architecture with both layouts.
56
+
57
+ ## What you get
58
+
59
+ **Application repositories** (one for a monorepo, one per app for split-repo):
60
+
61
+ - a small, dependency-free Python app per workload with a `/health` endpoint (services and the
62
+ monolith) or a run loop (workers), plus tests; replace it with your own code
63
+ - a `Dockerfile` and a `score.yaml` describing how the workload runs
64
+ - a Dagger module (`.dagger/`) with `test`, `build` and `ship` functions that run identically on
65
+ your laptop and in CI
66
+ - a CI file for your provider (`.github/workflows/ci.yaml` or `.gitlab-ci.yml`) that tests pull
67
+ requests and ships `main`
68
+
69
+ **A GitOps repository:**
70
+
71
+ - an Argo CD app-of-apps (`bootstrap/root.yaml`) and one ApplicationSet per environment
72
+ - a Helm chart that deploys any workload straight from its `score.yaml`
73
+ - per-environment values, and `release.yaml` files written by CI on every ship
74
+ - `scripts/bootstrap-local.sh` to run everything on a local [k3d](https://k3d.io) cluster
75
+
76
+ The flow: push to `main` → CI tests, builds and pushes images tagged with the commit → CI writes
77
+ `environments/dev/<workload>/release.yaml` → Argo CD deploys it. Promote by copying a
78
+ `release.yaml` to another environment.
79
+
80
+ ## Grow a project
81
+
82
+ Add a service or worker to a generated project:
83
+
84
+ ```sh
85
+ cd atlas
86
+ plinth add service billing --port 8090
87
+ plinth add worker mailer
88
+ ```
89
+
90
+ plinth creates the new workload's files (and a new repository for split-repo layouts) and
91
+ updates `plinth.yaml`. Adding to a modular monolith turns it into a hybrid. Existing files are
92
+ never overwritten; any that plinth would now generate differently are listed for you to review.
93
+
94
+ ## Commands
95
+
96
+ | Command | What it does |
97
+ |---|---|
98
+ | `plinth project [--output DIR]` | interactive questions, then generate |
99
+ | `plinth validate --config FILE` | check a config and show the repositories it produces |
100
+ | `plinth generate --config FILE --output DIR [--dry-run]` | write the repositories |
101
+ | `plinth add {service,worker} NAME [--port N]` | add a workload to an existing project |
102
+
103
+ `--no-git` skips `git init` and the initial commit.
104
+
105
+ ## Configuration
106
+
107
+ | Key | Notes |
108
+ |---|---|
109
+ | `project.name` | kebab-case, 2-30 chars; prefixes every repository name |
110
+ | `project.architecture` | `modular-monolith` (one monolith), `microservices` (services/workers only), `hybrid` (one monolith plus services/workers) |
111
+ | `project.layout` | `monorepo` (`<name>-app` holds every workload) or `split-repo` (`<name>-<app>` each) |
112
+ | `project.gitProvider` | picks the CI file; `generic` generates none |
113
+ | `project.gitBaseUrl` | `https://host/org` the repositories live under |
114
+ | `project.registry` | `host/path` images are pushed to |
115
+ | `applications[]` | `name`, `kind` (`modular-monolith`, `service`, `worker`), optional `port` for HTTP kinds |
116
+ | `environments` | must include `dev`, which CI releases to |
117
+
118
+ Defaults and pinned tool versions are in [`plinth/config.py`](plinth/config.py).
119
+
120
+ ## When not to use plinth
121
+
122
+ - You only need a single repository from a template: [Copier](https://copier.readthedocs.io)
123
+ or [Cookiecutter](https://cookiecutter.readthedocs.io) are simpler, and Copier can update
124
+ projects later.
125
+ - You already run [Backstage](https://backstage.io): its software templates fit better there.
126
+ - You don't deploy to Kubernetes: the GitOps half won't help you.
127
+
128
+ plinth generates once. It does not keep generated repositories in sync with newer templates;
129
+ after generation the code is yours.
130
+
131
+ ## Development
132
+
133
+ ```sh
134
+ uv sync
135
+ uv run pytest
136
+ uv run ruff check . && uv run ruff format --check .
137
+ ```
138
+
139
+ Templates live in [`plinth/blueprints/`](plinth/blueprints/). Files ending in `.j2` are rendered
140
+ with Jinja; path segments starting with `dot-` are written with a leading `.`.
141
+
142
+ ## Contributing
143
+
144
+ Contributions are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) first; in short, open an
145
+ issue before large changes, and every contribution must be written by a human (no AI-generated
146
+ code or text).
147
+
148
+ ## License
149
+
150
+ [MIT](LICENSE)
@@ -0,0 +1,12 @@
1
+ project:
2
+ name: atlas
3
+ architecture: hybrid
4
+ layout: monorepo
5
+ gitProvider: github
6
+ gitBaseUrl: https://github.com/acme
7
+ registry: ghcr.io/acme
8
+ applications:
9
+ - {name: api, kind: modular-monolith}
10
+ - {name: worker, kind: worker}
11
+ - {name: notifier, kind: service, port: 8081}
12
+ environments: [dev, production]
@@ -0,0 +1,12 @@
1
+ project:
2
+ name: atlas
3
+ architecture: hybrid
4
+ layout: split-repo
5
+ gitProvider: github
6
+ gitBaseUrl: https://github.com/acme
7
+ registry: ghcr.io/acme
8
+ applications:
9
+ - {name: api, kind: modular-monolith}
10
+ - {name: worker, kind: worker}
11
+ - {name: notifier, kind: service, port: 8081}
12
+ environments: [dev, production]
@@ -0,0 +1,12 @@
1
+ project:
2
+ name: atlas
3
+ architecture: microservices
4
+ layout: monorepo
5
+ gitProvider: github
6
+ gitBaseUrl: https://github.com/acme
7
+ registry: ghcr.io/acme
8
+ applications:
9
+ - {name: api, kind: service}
10
+ - {name: worker, kind: worker}
11
+ - {name: notifier, kind: service, port: 8081}
12
+ environments: [dev, production]
@@ -0,0 +1,12 @@
1
+ project:
2
+ name: atlas
3
+ architecture: microservices
4
+ layout: split-repo
5
+ gitProvider: github
6
+ gitBaseUrl: https://github.com/acme
7
+ registry: ghcr.io/acme
8
+ applications:
9
+ - {name: api, kind: service}
10
+ - {name: worker, kind: worker}
11
+ - {name: notifier, kind: service, port: 8081}
12
+ environments: [dev, production]
@@ -0,0 +1,10 @@
1
+ project:
2
+ name: atlas
3
+ architecture: modular-monolith
4
+ layout: monorepo
5
+ gitProvider: github
6
+ gitBaseUrl: https://github.com/acme
7
+ registry: ghcr.io/acme
8
+ applications:
9
+ - {name: api, kind: modular-monolith}
10
+ environments: [dev, production]
@@ -0,0 +1,10 @@
1
+ project:
2
+ name: atlas
3
+ architecture: modular-monolith
4
+ layout: split-repo
5
+ gitProvider: github
6
+ gitBaseUrl: https://github.com/acme
7
+ registry: ghcr.io/acme
8
+ applications:
9
+ - {name: api, kind: modular-monolith}
10
+ environments: [dev, production]
@@ -0,0 +1 @@
1
+ """plinth: generate ready-to-push application and GitOps repositories from one config."""
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from plinth.cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,97 @@
1
+ """`plinth add`: grow an existing project with a new service or worker.
2
+
3
+ Only files that belong to the new workload are created. Existing files are never
4
+ overwritten; ones that plinth would now generate differently are reported so the
5
+ user can review them.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+
13
+ from plinth import config as defaults
14
+ from plinth.loader import ConfigError, config_to_dict, dump_config, load_config, parse_config
15
+ from plinth.plan import build_plan
16
+ from plinth.render import render_plan
17
+ from plinth.schema import AppKind, Architecture, ProjectConfig
18
+ from plinth.write import init_repo, write_tree
19
+
20
+
21
+ @dataclass
22
+ class AddResult:
23
+ config: ProjectConfig
24
+ created: list[str] = field(default_factory=list)
25
+ kept: list[str] = field(default_factory=list)
26
+ new_repos: list[str] = field(default_factory=list)
27
+ notes: list[str] = field(default_factory=list)
28
+ warnings: list[str] = field(default_factory=list)
29
+
30
+
31
+ def add_application(
32
+ project_dir: Path, kind: AppKind, name: str, port: int | None = None, init_git: bool = True
33
+ ) -> AddResult:
34
+ config_path = project_dir / defaults.CONFIG_FILENAME
35
+ old = load_config(config_path)
36
+ data = config_to_dict(old)
37
+ notes = []
38
+
39
+ if kind is AppKind.MODULAR_MONOLITH:
40
+ raise ConfigError("a project has exactly one modular-monolith; add a service or worker")
41
+ if old.project.architecture is Architecture.MODULAR_MONOLITH:
42
+ data["project"]["architecture"] = Architecture.HYBRID.value
43
+ notes.append("architecture changed from modular-monolith to hybrid")
44
+
45
+ app = {"name": name, "kind": kind.value}
46
+ if port is not None:
47
+ app["port"] = port
48
+ data["applications"].append(app)
49
+ new = parse_config(data, source=str(config_path))
50
+
51
+ old_plan, new_plan = build_plan(old), build_plan(new)
52
+ old_folders = {r.folder for r in old_plan.repos}
53
+ files = render_plan(new_plan)
54
+ result = AddResult(config=new, notes=notes)
55
+
56
+ new_repos = [r for r in new_plan.repos if r.folder not in old_folders]
57
+ for repo in new_repos:
58
+ if (project_dir / repo.folder).exists():
59
+ raise ConfigError(f"cannot create repository folder, it already exists: {repo.folder}")
60
+
61
+ owned = _new_workload_prefixes(new_plan, name)
62
+ to_write = {}
63
+ for rel, content in sorted(files.items()):
64
+ folder = rel.split("/", 1)[0]
65
+ dest = project_dir / rel
66
+ if folder not in old_folders or rel.startswith(owned):
67
+ if dest.exists():
68
+ if dest.read_bytes() != content:
69
+ result.kept.append(rel)
70
+ continue
71
+ to_write[rel] = content
72
+ result.created.append(rel)
73
+ elif dest.exists() and dest.read_bytes() != content:
74
+ result.kept.append(rel)
75
+
76
+ write_tree(project_dir, to_write)
77
+ config_path.write_text(dump_config(new), encoding="utf-8")
78
+
79
+ for repo in new_repos:
80
+ result.new_repos.append(repo.folder)
81
+ if init_git:
82
+ warning = init_repo(project_dir / repo.folder, repo.url)
83
+ if warning:
84
+ result.warnings.append(warning)
85
+ return result
86
+
87
+
88
+ def _new_workload_prefixes(plan, name: str) -> tuple[str, ...]:
89
+ prefixes = [
90
+ f"{repo.folder}/{wl.source_dir}"
91
+ for repo, wl in plan.workloads()
92
+ if wl.name == name and wl.source_dir
93
+ ]
94
+ prefixes += [
95
+ f"{plan.gitops.folder}/environments/{env}/{name}/" for env in plan.config.environments
96
+ ]
97
+ return tuple(prefixes)
@@ -0,0 +1,38 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+ packages: write
11
+
12
+ jobs:
13
+ ci:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: {{ defaults.CHECKOUT_ACTION }}
17
+
18
+ - name: Test
19
+ if: github.event_name == 'pull_request'
20
+ uses: {{ defaults.DAGGER_GITHUB_ACTION }}
21
+ with:
22
+ version: "{{ defaults.DAGGER_VERSION }}"
23
+ call: test
24
+
25
+ - name: Ship
26
+ if: github.event_name == 'push'
27
+ uses: {{ defaults.DAGGER_GITHUB_ACTION }}
28
+ env:
29
+ REGISTRY_PASSWORD: {{ "${{ secrets.REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}" }}
30
+ GITOPS_TOKEN: {{ "${{ secrets.GITOPS_TOKEN }}" }}
31
+ with:
32
+ version: "{{ defaults.DAGGER_VERSION }}"
33
+ call: >-
34
+ ship
35
+ --commit={{ "${{ github.sha }}" }}
36
+ --registry-username={{ "${{ vars.REGISTRY_USERNAME || github.actor }}" }}
37
+ --registry-password=env://REGISTRY_PASSWORD
38
+ --git-token=env://GITOPS_TOKEN
@@ -0,0 +1,36 @@
1
+ stages: [test, ship]
2
+
3
+ .dagger:
4
+ image: {{ defaults.DOCKER_IMAGE }}
5
+ services:
6
+ - {{ defaults.DOCKER_IMAGE }}-dind
7
+ variables:
8
+ DOCKER_HOST: tcp://docker:2376
9
+ DOCKER_TLS_CERTDIR: /certs
10
+ DOCKER_TLS_VERIFY: "1"
11
+ DOCKER_CERT_PATH: /certs/client
12
+ DAGGER_VERSION: "{{ defaults.DAGGER_VERSION }}"
13
+ before_script:
14
+ - apk add --no-cache curl
15
+ - curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin sh
16
+
17
+ test:
18
+ extends: .dagger
19
+ stage: test
20
+ rules:
21
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
22
+ script:
23
+ - dagger call test
24
+
25
+ ship:
26
+ extends: .dagger
27
+ stage: ship
28
+ rules:
29
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
30
+ script:
31
+ - >-
32
+ dagger call ship
33
+ --commit=$CI_COMMIT_SHA
34
+ --registry-username=$REGISTRY_USERNAME
35
+ --registry-password=env://REGISTRY_PASSWORD
36
+ --git-token=env://GITOPS_TOKEN
@@ -0,0 +1,46 @@
1
+ apiVersion: argoproj.io/v1alpha1
2
+ kind: ApplicationSet
3
+ metadata:
4
+ name: {{ project.name }}-{{ environment }}
5
+ namespace: argocd
6
+ spec:
7
+ goTemplate: true
8
+ goTemplateOptions: ["missingkey=error"]
9
+ generators:
10
+ - git:
11
+ repoURL: {{ repo.url }}
12
+ revision: HEAD
13
+ files:
14
+ - path: environments/{{ environment }}/*/release.yaml
15
+ template:
16
+ metadata:
17
+ name: '{{ project.name }}-{% raw %}{{ .workload }}{% endraw %}-{{ environment }}'
18
+ spec:
19
+ project: {{ project.name }}
20
+ destination:
21
+ server: https://kubernetes.default.svc
22
+ namespace: {{ project.name }}-{{ environment }}
23
+ sources:
24
+ - repoURL: {{ repo.url }}
25
+ targetRevision: HEAD
26
+ path: charts/workload
27
+ helm:
28
+ releaseName: '{% raw %}{{ .workload }}{% endraw %}'
29
+ valueFiles:
30
+ - '$app/{% raw %}{{ .scorePath }}{% endraw %}'
31
+ - '$gitops/environments/{{ environment }}/{% raw %}{{ .workload }}{% endraw %}/values.yaml'
32
+ parameters:
33
+ - name: image
34
+ value: '{% raw %}{{ .image }}{% endraw %}'
35
+ - repoURL: '{% raw %}{{ .appRepoURL }}{% endraw %}'
36
+ targetRevision: '{% raw %}{{ .revision }}{% endraw %}'
37
+ ref: app
38
+ - repoURL: {{ repo.url }}
39
+ targetRevision: HEAD
40
+ ref: gitops
41
+ syncPolicy:
42
+ automated:
43
+ prune: false
44
+ selfHeal: true
45
+ syncOptions:
46
+ - CreateNamespace=true
@@ -0,0 +1,2 @@
1
+ # {{ workload.name }} in {{ environment }}. Merged over the workload's score.yaml.
2
+ replicas: {{ defaults.REPLICAS.get(environment, defaults.DEFAULT_REPLICAS) }}