docs: add installer bootstrap design
This commit is contained in:
commit
9d4444f4ff
1 changed files with 215 additions and 0 deletions
|
|
@ -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.
|
||||
Loading…
Reference in a new issue