215 lines
8 KiB
Markdown
215 lines
8 KiB
Markdown
# 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.
|