1
0
Fork 0

docs: add installer bootstrap design

This commit is contained in:
thibaud-leclere 2026-05-05 09:11:59 +02:00
commit 9d4444f4ff

View file

@ -0,0 +1,215 @@
# Get Installer Bootstrap Design
## Goal
Build a small web application that serves cached `.sh` installer scripts from
Git repositories through stable public paths. The primary use case is
bootstrapping installation scripts for projects through URLs such as
`/mcp/graylog/install.sh`, while keeping repository details and access tokens
managed from an admin interface.
## Scope
The first version includes:
- A public endpoint that serves cached shell scripts for configured paths.
- An `/admin` interface with a login screen.
- Admin account storage in SQLite with hashed passwords.
- CRUD for script mappings.
- Optional per-mapping Git access token support for private repositories.
- Manual synchronization from Git into a local cache.
- Docker support for production deployment through Coolify.
- Docker Compose support for local development with the source directory mounted
into the PHP container.
The first version does not include:
- Multi-tenant access control.
- Scheduled background synchronization.
- Webhook-triggered synchronization.
- Multiple active application replicas sharing the same SQLite database.
- Public directory browsing or repository browsing.
## Technology
Use PHP 8.3 with a minimal Symfony application. Symfony provides routing,
forms, CSRF protection, password hashing, Doctrine integration, migrations, and
test support without needing to reimplement those concerns.
Use SQLite by default through `DATABASE_URL`. This is enough for one deployed
instance and keeps Coolify deployment simple. The application should keep the
database configuration compatible with PostgreSQL so that a future deployment
can switch to PostgreSQL by changing `DATABASE_URL` and adding a database
service.
Use Nginx in front of PHP-FPM. The production image copies application sources
into the image. The development Compose file mounts the local source tree into
the container so code changes can be tested without rebuilding.
## Main Components
### Public Script Serving
The public controller receives all non-admin paths and resolves them against an
active mapping. A configured public path such as `mcp/graylog/install.sh`
matches the request path `/mcp/graylog/install.sh`.
The public endpoint never contacts Git directly. It serves only the current
cached file for the mapping. If no active mapping exists, or if the mapping has
never synchronized successfully, the endpoint returns `404`.
Served files should use a shell-friendly content type such as
`text/x-shellscript; charset=UTF-8`, with `Content-Disposition: inline`.
### Admin Interface
`/admin` displays a login screen when the user is not authenticated. Admin
users are stored in the application database and passwords are hashed with
Symfony's password hasher.
After login, the admin can:
- List mappings.
- Create a mapping.
- Edit a mapping.
- Delete a mapping.
- Trigger synchronization for one mapping.
- See the last synchronization status, last synchronization timestamp, and last
synchronization error.
All admin forms use CSRF protection.
### Mapping Model
Each mapping stores:
- Public path, for example `mcp/graylog/install.sh`.
- Git repository URL, for example
`https://forge.lclr.dev/AI/graylog-mcp.git`.
- Git ref, for example `main`.
- File path inside the repository, for example `install.sh`.
- Optional access token.
- Active flag.
- Last synchronization status.
- Last successful synchronization timestamp.
- Last synchronization error.
- Cached file path or cache key.
Public paths and repository file paths are normalized before storage and before
use. They must be relative paths, must not contain `..`, and public paths must
end in `.sh`.
### Git Synchronization
Synchronization is manual from the admin interface.
For each mapping, the synchronizer:
1. Creates or reuses a per-mapping working directory under a cache volume.
2. Clones the repository when the local working directory does not exist.
3. Fetches updates when it already exists.
4. Checks out the configured ref.
5. Validates that the configured repository file path resolves inside the
checkout.
6. Validates that the target is a regular file.
7. Copies the file into a stable served cache location for the mapping.
8. Updates synchronization metadata in the database.
If synchronization fails, the previous served cache remains in place. The
mapping records the error so the admin can diagnose the issue without breaking
already working installer URLs.
For private HTTPS repositories, the synchronizer uses the configured access
token only during Git operations. The token is never exposed in public responses
or admin list views. Forms may allow replacing or clearing a token without
displaying the stored value.
### Storage Layout
The application uses two persistent locations:
- Database volume: stores SQLite database files.
- Cache volume: stores Git working directories and served script copies.
The application never serves files directly from arbitrary disk paths. Public
responses always go through the mapping lookup and cache resolver.
## Docker And Deployment
Production deployment uses Docker Compose with at least:
- An app/PHP-FPM container built from the repository.
- An Nginx container using a checked-in Nginx config.
- Volumes for the SQLite database and Git/script cache.
The production Dockerfile copies source files into the image and installs PHP
dependencies during the image build.
Development uses a Compose override or a dedicated development Compose file
that mounts the local repository into the app container. This allows editing
Symfony files locally and testing immediately without rebuilding the image.
## Security
Admin authentication is required for every `/admin` route except login.
Tokens are sensitive data. They must not be logged, displayed in list pages, or
returned by public endpoints. Error messages should identify which mapping
failed and why, without printing credentials or credential-bearing Git URLs.
Path handling is strict:
- Public paths are relative.
- Public paths cannot contain empty segments, `.` segments, or `..` segments.
- Public paths must end with `.sh`.
- Repository file paths are relative.
- Repository file paths cannot escape the checkout directory.
- Public serving cannot fall back to arbitrary files on disk.
The admin interface uses CSRF protection for state-changing actions.
## Error Handling
Public endpoint behavior:
- Unknown path: `404`.
- Known mapping without successful cache: `404`.
- Inactive mapping: `404`.
- Unexpected read failure: `500`, with details in server logs only.
Admin synchronization behavior:
- Git failures are stored on the mapping as the last synchronization error.
- The previous cached script remains available when present.
- The admin screen shows success or failure after synchronization.
Production logs should identify the mapping, public path, repository host/path
when useful, and the operation that failed. Logs must not include access tokens.
## Testing
The implementation should include focused tests for:
- Public path normalization and rejection of dangerous paths.
- Repository file path normalization and checkout escape prevention.
- Public endpoint returns cached script content for a valid synchronized
mapping.
- Public endpoint returns `404` for unknown, inactive, or unsynchronized
mappings.
- Admin login protects `/admin`.
- Mapping creation validates required fields and `.sh` public paths.
- Synchronization preserves the previous cached file when Git update fails.
Git synchronization can be tested against local fixture repositories to avoid
network dependency.
## Regression Risk
The highest risk areas are path handling, token handling, and cache update
atomicity. The implementation should keep these responsibilities isolated in
small services so the public controller, admin forms, and synchronizer do not
duplicate security-sensitive logic.
SQLite is acceptable for the first deployment because the app is intended to run
as a single instance. If the deployment later needs multiple active replicas,
PostgreSQL should replace SQLite before scaling horizontally.