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.
- plinth_cli-0.2.0/.gitignore +8 -0
- plinth_cli-0.2.0/LICENSE +21 -0
- plinth_cli-0.2.0/PKG-INFO +174 -0
- plinth_cli-0.2.0/README.md +150 -0
- plinth_cli-0.2.0/examples/hybrid-monorepo/plinth.yaml +12 -0
- plinth_cli-0.2.0/examples/hybrid-split-repo/plinth.yaml +12 -0
- plinth_cli-0.2.0/examples/microservices-monorepo/plinth.yaml +12 -0
- plinth_cli-0.2.0/examples/microservices-split-repo/plinth.yaml +12 -0
- plinth_cli-0.2.0/examples/modular-monolith-monorepo/plinth.yaml +10 -0
- plinth_cli-0.2.0/examples/modular-monolith-split-repo/plinth.yaml +10 -0
- plinth_cli-0.2.0/plinth/__init__.py +1 -0
- plinth_cli-0.2.0/plinth/__main__.py +5 -0
- plinth_cli-0.2.0/plinth/add.py +97 -0
- plinth_cli-0.2.0/plinth/blueprints/ci/github/dot-github/workflows/ci.yaml.j2 +38 -0
- plinth_cli-0.2.0/plinth/blueprints/ci/gitlab/dot-gitlab-ci.yml.j2 +36 -0
- plinth_cli-0.2.0/plinth/blueprints/gitops/applicationset.yaml.j2 +46 -0
- plinth_cli-0.2.0/plinth/blueprints/gitops/values.yaml.j2 +2 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/app/README.md.j2 +63 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/app/dagger.json.j2 +8 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/app/dot-dagger/dot-gitignore +3 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/app/dot-dagger/pyproject.toml +12 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/app/dot-dagger/src/ci/__init__.py +3 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/app/dot-dagger/src/ci/main.py.j2 +133 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/app/dot-gitignore +4 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/gitops/README.md.j2 +53 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/gitops/apps/project.yaml.j2 +15 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/gitops/bootstrap/root.yaml.j2 +20 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/gitops/charts/workload/Chart.yaml +5 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/gitops/charts/workload/templates/_helpers.tpl +13 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/gitops/charts/workload/templates/deployment.yaml +61 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/gitops/charts/workload/templates/service.yaml +20 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/gitops/charts/workload/values.yaml +9 -0
- plinth_cli-0.2.0/plinth/blueprints/repo/gitops/scripts/bootstrap-local.sh.j2 +23 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/common/Dockerfile.j2 +14 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/common/app/__init__.py +0 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/common/dot-dockerignore +4 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/common/score.yaml.j2 +33 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/common/tests/__init__.py +0 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/modular-monolith/app/main.py.j2 +63 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/modular-monolith/app/modules/__init__.py +5 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/modular-monolith/app/modules/example.py +12 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/modular-monolith/tests/test_main.py.j2 +42 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/service/app/main.py.j2 +54 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/service/tests/test_main.py.j2 +41 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/worker/app/main.py.j2 +44 -0
- plinth_cli-0.2.0/plinth/blueprints/workload/worker/tests/test_main.py.j2 +26 -0
- plinth_cli-0.2.0/plinth/cli.py +185 -0
- plinth_cli-0.2.0/plinth/config.py +51 -0
- plinth_cli-0.2.0/plinth/loader.py +49 -0
- plinth_cli-0.2.0/plinth/plan.py +70 -0
- plinth_cli-0.2.0/plinth/render.py +119 -0
- plinth_cli-0.2.0/plinth/schema.py +158 -0
- plinth_cli-0.2.0/plinth/wizard.py +129 -0
- plinth_cli-0.2.0/plinth/write.py +72 -0
- plinth_cli-0.2.0/pyproject.toml +56 -0
- plinth_cli-0.2.0/tests/__init__.py +0 -0
- plinth_cli-0.2.0/tests/conftest.py +48 -0
- plinth_cli-0.2.0/tests/test_add.py +92 -0
- plinth_cli-0.2.0/tests/test_cli.py +97 -0
- plinth_cli-0.2.0/tests/test_generate.py +169 -0
- plinth_cli-0.2.0/tests/test_plan.py +52 -0
- plinth_cli-0.2.0/tests/test_schema.py +134 -0
- plinth_cli-0.2.0/tests/test_wizard.py +73 -0
- plinth_cli-0.2.0/tests/test_write.py +58 -0
plinth_cli-0.2.0/LICENSE
ADDED
|
@@ -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 @@
|
|
|
1
|
+
"""plinth: generate ready-to-push application and GitOps repositories from one config."""
|
|
@@ -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
|