diff --git a/.forgejo/workflows/book-pr.yaml b/.forgejo/workflows/book-pr.yaml new file mode 100644 index 0000000..7f978d1 --- /dev/null +++ b/.forgejo/workflows/book-pr.yaml @@ -0,0 +1,61 @@ +name: "Book - PR" + +on: + pull_request: + types: + - opened + - reopened + - synchronize + paths: + - ".forgejo/workflows/book-pr.yaml" + - "book/**" + +concurrency: + group: page-preview + cancel-in-progress: false + +jobs: + createPreview: + runs-on: nix + env: + SITE_ORIGIN: + "https://${{ forge.event.repository.owner.username + }}.preview.codeberg.page" + SITE_BASEPATH: + "/${{ forge.event.repository.name }}@${{ forge.event.number }}/" + steps: + - uses: actions/checkout@v4 + - uses: https://codeberg.org/icewind/attic-action@v1 + with: + name: link + instance: https://cache.icewind.link + authToken: "${{ secrets.ATTIC_TOKEN }}" + - name: Build + run: | + # git-pages/action doesn't like symlinks + cp -r $(nix build .#haze-book --print-out-paths --no-link) dist + - name: "Deploy Site" + uses: https://code.forgejo.org/actions/git-pages@v2 + with: + site: "${{ env.SITE_ORIGIN }}${{ env.SITE_BASEPATH }}" # + token: "${{ forge.token }}" # + source: "dist/" # + - name: "Find comment" + uses: "https://github.com/peter-evans/find-comment@v4" # + id: comment + with: + issue-number: ${{ forge.event.number }} # + body-includes: "" # + - name: "Create or update comment" + uses: "https://github.com/peter-evans/create-or-update-comment@v5" # + with: + issue-number: ${{ forge.event.number }} # + comment-id: "${{ steps.comment.outputs.comment-id }}" # + edit-mode: "replace" + body: |- + # Pull request preview ready! + + The Preview of commit ${{ forge.event.pull_request.head.sha }} has been created. + You can find it here: ${{ env.SITE_ORIGIN }}${{ env.SITE_BASEPATH }} + + diff --git a/.forgejo/workflows/book.yaml b/.forgejo/workflows/book.yaml new file mode 100644 index 0000000..05b4f11 --- /dev/null +++ b/.forgejo/workflows/book.yaml @@ -0,0 +1,29 @@ +name: "Book" + +on: + push: + branches: + - main + +jobs: + preview: + runs-on: nix + env: + SITE_ORIGIN: "${{forge.event.repository.owner.username}}.codeberg.page" + SITE_BASEPATH: "/${{forge.event.repository.name}}/" + steps: + - uses: actions/checkout@v4 + - uses: https://codeberg.org/icewind/attic-action@v1 + with: + name: link + instance: https://cache.icewind.link + authToken: "${{ secrets.ATTIC_TOKEN }}" + - name: Build + run: | + # git-pages/action doesn't like symlinks + cp -r $(nix build .#haze-book --print-out-paths --no-link) dist + - uses: https://codeberg.org/git-pages/action@v2 + with: + site: https://${{ env.SITE_ORIGIN }}${{ env.SITE_BASEPATH }} + token: ${{ forge.token }} + source: dist/ diff --git a/.forgejo/workflows/docker.yaml b/.forgejo/workflows/docker.yaml index 244919b..4cbab3d 100644 --- a/.forgejo/workflows/docker.yaml +++ b/.forgejo/workflows/docker.yaml @@ -4,13 +4,11 @@ on: push: branches: ["main"] paths: + - "flake.*" - "Cargo.toml" - ".forgejo/workflows/docker.yaml" - "nix/image/**" -permissions: - contents: read - jobs: build-images: runs-on: nix diff --git a/.gitignore b/.gitignore index 512d7c7..87a4f44 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .direnv .env result +.vale/* +!.vale/config diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 0000000..1f31616 --- /dev/null +++ b/.vale.ini @@ -0,0 +1,13 @@ +StylesPath = .vale +MinAlertLevel = suggestion +Vocab = haze + +[formats] +mdx = md + +[*] +BasedOnStyles = Vale + +[*.{md,mdx}] +BasedOnStyles = Vale +Microsoft.Contractions = NO diff --git a/.vale/config/vocabularies/haze/accept.txt b/.vale/config/vocabularies/haze/accept.txt new file mode 100644 index 0000000..925d628 --- /dev/null +++ b/.vale/config/vocabularies/haze/accept.txt @@ -0,0 +1,26 @@ +Nextcloud +ownCloud +appstore +Authentik +Caddy +cron +FrankenPHP +PHPUnit +mdBook +boolean +Codeberg +Kaspersky +Redis +Podman +Xdebug +occ +dnsmasq +notify_push +sharding +tokio +worktree +worktrees +config +stdin +stdout +direnv diff --git a/CHANGELOG.md b/CHANGELOG.md index d158b83..4de8b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,37 +1,51 @@ +## 2.4.2 + +- Support having a `#!` inside a haze script to specify the interpreter to use +- Add WebUI for accessing the database. +- Allow opening services with `haze open [service]` + +## 2.4.1 + +- Show database location on start +- Direct TLS listening support for the proxy +- Set `SERVER_NAME` for FrankenPHP +- Expose Caddy admin endpoints when using FrankenPHP +- Use cron for background jobs + ## 2.4.0 - Show instance details in shell prompt - Add option to start an instance with a detached worktree - Add "haze scripts" that bundle a setup configuration and script to run in the instance -- Allow specifying phpunit version when running tests -- Add SAML service using authentik -- Add OIDC service using authentik -- Add SCIM service using authentik +- Add option to specify PHPUnit version when running tests +- Add SAML service using Authentik +- Add OIDC service using Authentik +- Add SCIM service using Authentik - Fix using a single word as instance name - Fixed cleanup not removing some cache files -- Remove memory and cpu limits from containers +- Remove memory and CPU limits from containers - Fix max upload size not being set correctly ## 2.3.0 -- Allow execing into service containers -- Add sftp with key authentication service -- Fix mysql 8 support +- Add option to `exec` in service containers +- Add SFTP with key authentication service +- Fix MySQL 8 support ## 2.2.2 - parallelize `git pull` - add webhook tester -- automatically configure ldap when enabled -- improve compatibility with intergration tests +- automatically configure LDAP when enabled +- improve compatibility with integration tests - add `php-imagick` module ## 2.1.1 -- Add basic [frankenphp](https://github.com/php/frankenphp) support +- Add basic [FrankenPHP](https://github.com/php/frankenphp) support - Allow running integration tests from (some) apps -- Support federation with proxy and using 127.0.0.1 as proxy ip +- Support federation with proxy and using 127.0.0.1 as proxy IP - Fix office not working - Warn when using out-of-date images. @@ -41,8 +55,8 @@ - Faster stopping of instances, by @provokateurin - Support extra app directories - Enable appstore -- Allow setting config options pre-setup +- Allow setting configuration options pre-setup - Improved access to service containers - Add S3 with TLS options -- Updated php 8.0 and 8.1 images -- Add php 8.5 support +- Updated PHP 8.0 and 8.1 images +- Add PHP 8.5 support diff --git a/Cargo.lock b/Cargo.lock index 9a62e09..bbaba4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -899,7 +899,7 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "haze" -version = "2.4.0" +version = "2.4.2" dependencies = [ "async-trait", "atty", @@ -933,6 +933,7 @@ dependencies = [ "tar", "termion", "tokio", + "tokio-rustls", "tokio-stream", "toml", "tracing", @@ -2016,6 +2017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", + "log", "once_cell", "rustls-pki-types", "rustls-webpki", diff --git a/Cargo.toml b/Cargo.toml index 324dd21..3968b51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "haze" -version = "2.4.0" +version = "2.4.2" edition = "2024" description = "Easy setup and management of Nextcloud test instances using docker" repository = "https://codeberg.org/icewind/haze" @@ -44,6 +44,7 @@ tokio = { version = "1.53.1", features = [ "rt-multi-thread", "signal" ] } +tokio-rustls = "0.26" tokio-stream = { version = "0.1.19", features = ["net"] } toml = "1.1.4" tracing = "0.1.44" diff --git a/DEVELOPING.md b/DEVELOPING.md new file mode 100644 index 0000000..d7f9f65 --- /dev/null +++ b/DEVELOPING.md @@ -0,0 +1,80 @@ +# Developing + +## Setup + +### Nix based development environment + +The recommended way to setup the development environment is to use +[Lix](https://lix.systems/install/)/[Nix](https://nixos.org/download/). + +Once installed, you can enter the development shell using `nix develop` or, +setup [direnv](https://direnv.net/) to automatically enter the development shell +when you enter the folder. + +The development shell provides all necessary tooling to develop haze. + +### Without Nix + +The fooling tools need to be setup to develop without using Nix to manage the +development setup: + +- [`cargo`](https://doc.rust-lang.org/cargo/getting-started/installation.html) - + For building the program +- [`mdbook`](https://github.com/rust-lang/mdBook) - For building the + documentation +- [`prettier`](https://prettier.io/) - For formatting the non-rust files +- [`vale`](https://vale.sh/) - For checking documentation + +## Building + +```bash +cargo build +``` + +The binary can then be found at `target/debug/haze`. + +## Formatting + +### With Nix + +```bash +nix fmt +``` + +### Without Nix + +#### Rust code + +```bash +cargo fmt +``` + +#### Documentation + +```bash +prettier --write '**/*.md' +``` + +### Building documentation + +```bash +mdbook serve +``` + +### Checking documentation + +```bash +vale src/ book/src/ book/README.md *.md +``` + +### Run all checks + +```bash +nix flake check +``` + +### Building docker images + +```bash +nix run .#'"haze-image-php-8.4"'.copyToDockerDaemon +``` diff --git a/README.md b/README.md index d5a3f9b..33aee03 100644 --- a/README.md +++ b/README.md @@ -7,473 +7,35 @@ Easy setup and management of Nextcloud test instances using docker ## What `haze` provides an easy way to set up Nextcloud test instances with a choice of -php version, database server, optional s3 or ldap setup and more. +PHP version, database server, optional s3 or LDAP setup and more. -## Setup +## Documentation -### Requirements +Documentation for haze can be found in the +[book](https://icewind.codeberg.page/haze/) -- Docker - -### Installation +## Quickstart - Grab a binary from the [Codeberg releases](https://codeberg.org/icewind/haze/releases) and place it in your `$PATH` -### Config +- Create a file `~/.config/haze/haze.toml` with the following content: -Create a file `~/.config/haze/haze.toml` with the following options: - -```toml -sources_root = "/path/to/nextcloud/sources" -``` - -See the [configuration section](#configuration) for more options. - -### Quick examples - -- Start a Nextcloud instance with `postgresql`, and `s3` primary storage: - - ```bash - haze start pgsql s3 + ```toml + sources_root = "/path/to/nextcloud/sources" ``` -- Start a Nextcloud instance with `sqlite`, `php 8.3` and an `smb` external - storage: +- Start a basic Nextcloud instance: ```bash - haze start 8.3 smb + haze start ``` -- Run specific units test against an `oracle` database - ```bash - haze test oracle apps/dav/tests/unit/Connector/Sabre - ``` +- Navigate to the address that is provided in the output. -## Managing instances +See the [book](https://icewind.codeberg.page/haze/) for more details. -#### Start an instance +## Developing -```bash -haze start [--name ] [--detach] [database] [php-version] [services] [vX.Y.Z] -``` - -Where `database` is one of `sqlite`, `mysql`, `mariadb`, `pgsql` or `oracle` -with an optional version (e.g. `pgsql:12`), defaults to `sqlite`. And -`php-version` is one of `8.0`, `8.1`, `8.2`, `8.3`, `8.4` or `8.5`, defaults to -the maximum version support by the current Nextcloud version. - -You can specify a version number (e.g. `v32.0.2`) to use the sources from a -release instead of using the local sources. - -Use `--name ` to give the instance a specific name instead of a randomly -generated one. For example: - -Use `--detach` to give the instance -[its own sources](#instances-with-their-own-sources) instead of sharing -`sources_root` with all other instances. - -Additionally, you can use the following options when starting an instance: - -- `s3`: set up an S3 server and configure to Nextcloud to use it as primary - storage. - - `s3s`: enable TLS for the S3 setup. - - `s3mb`: enable multi-bucket S3 setup. - - `s3m`: enable multi-instance S3 setup. -- `ldap`: set up an LDAP server. -- `saml`: set up authentik as a SAML IDP. -- `oidc`: set up authentik as an OIDC IDP. -- `scim`: set up authentik as a SCIM server. -- `office`: set up a Nextcloud Office server. -- `onlyoffice` setup an onlyoffice document server. -- `push` set up [client push](https://github.com/nextcloud/notify_push). -- `smb`: set up a samba server for external storage use. -- `dav`: set up a WebDAV server for external storage use. -- `sftp`: set up a SFTP server for external storage use. -- `sftp-key`: set up a SFTP server for external storage use with public key - authentication. -- `kaspersky`: set up a kaspersky scan engine server in http mode. ( Requires - [manually setting up the image](https://github.com/icewind1991/kaspersky-docker)) -- `kaspersky-icap`: setup a kaspersky scan engine server in ICAP mode. -- `clamav`: set up a local clam av scanner in executable mode. -- `clamav-socket`: set up a clam av scanner in socket mode. -- `clamav-icap`: set up a clam av scanner in ICAP mode. -- `clamav-icap-tls`: set up a clam av scanner in ICAP mode with TLS encryption. -- `oc`: start an ownCloud instance in the same network. -- `imaginary`: start an Imaginary service and configure it for preview - generation. -- `mail`: start a [smtp4dev](https://github.com/rnwood/smtp4dev) server and - configure it the mail server. -- `webhook` start a - [webhook tester](https://github.com/tarampampam/webhook-tester) -- `redis`: start a separate container for redis. -- `redis-tls`: connect to redis over TLS. -- ``: by specifying the path to an app package this package - will be extracted into the apps. directory of the new instance (overwriting - any existing app code). This can be used to quickly test a packaged app. -- The name of any configured preset. - -#### Run tests in a new instance - -```bash -haze test [database] [php-version] [phpunit version] [path] -``` - -Where `path` is a file or folder to run PHPUnit in, relative to the sources -root. - -### List running instances - -```bash -haze -``` - -or - -```bash -haze list -``` - -#### Remove all running instances - -```bash -haze clean -``` - -### Instances with their own sources - -By default every instance shares the sources configured as `sources_root`, so -all instances always run the same code. An instance started with `--detach` gets -its own `git worktree` of `sources_root` instead, created in `worktree_dir`, -which lets you run several instances on different branches at the same time. - -```bash -haze start --name my-fix --detach -``` - -The worktree is checked out with a detached head. Every app in one of the -`app_directories` that gets enabled during setup receives its own worktree as -well, other apps keep running from the shared app directory. - -The worktrees are removed together with the instance, by `haze stop` or -`haze clean`. - -## Controlling running instances - -The following commands run against the most recently started instance and allow -optionally providing a `match` to select a specific instance by its name. - -#### Open an instance - -```bash -haze [match] open -``` - -#### Open the database of an instance - -```bash -haze [match] db -``` - -#### Execute a command on an instance - -```bash -haze [match] [service] [cmd] -``` - -If no `cmd` is specified it will launch `bash` - -If a service name or `db` is provided, the command will be in the container of -the service or database. - -#### Create a new instance and run a command - -```bash -haze [match] shell [cmd] -``` - -If no `cmd` is specified it will launch `bash` - -#### Execute an occ command on an instance - -```bash -haze [match] occ [cmd] -``` - -#### Connect to the database on an instance - -```bash -haze [match] db -``` - -#### Show the logs of an instance - -```bash -haze [match] logs -``` - -#### Stop an instance - -```bash -haze [match] stop -``` - -#### Pin an instance - -```bash -haze [match] pin -``` - -Pinned instances will not be removed by `haze clean`. - -#### Unpin an instance - -```bash -haze [match] unpin -``` - -#### Run a command with instance environment variables set - -```bash -haze [match] env [args] -``` - -Runs the provided command with `NEXTCLOUD_URL`, `DATABASE_URL` and `REDIS_URL` -environment variables set for the matched instance. - -This is intended to run a local -[push daemon](https://github.com/nextcloud/notify_push) against an instance. - -#### Update the container images - -```bash -haze update -``` - -#### Edit a file in an instance with the local $EDITOR - -```bash -haze [match] edit -``` - -#### Reload the php config of an instance - -```bash -haze [match] reload -``` - -The php configuration can edit changed with `haze edit /config/php.ini` - -#### Checkout a branch for all local apps - -```bash -haze git checkout [branch] -``` - -Checks out the branch in all git repositories within the apps folder. - -Defaults to the branch matching the current checked out server versions (e.g. -`master` or `stable33`). - -`master` and `main` can be used interchangeably. - -#### Pull remote changes for all local apps - -```bash -haze git pull -``` - -Performs a pull in all git repositories within the apps folder. - -## Federation - -Multiple instances can reach each other by using their instance name as domain -name to allow for testing federation between instances. Alternatively, you can -set up the haze proxy and the proxied domains to get https support between -instances. - -## Proxy - -By default, instances can be accessed by their IP. In order to get more -memorable URLs and allow supporting https, haze comes with a builtin reverse -proxy to allow using a wildcard domain. - -### Requirements - -- A domain name you can set wildcard DNS records for -- A reverse proxy like Nginx or Apache -- (optionally) a wildcard ssl certificate (can be acquiring using letsencrypt - and dns verification) - -### DNS Setup - -- Set a DNS record for `*.haze.example.com` and `haze.example.com` pointing to - your development machine. -- Set the `proxy` configuration with your domain and desired listen endpoint. -- Set up a service to run `haze proxy` in the background as your own user. A - systemd user service is recommended (see [haze.service](./haze.service) for an - example). -- Configure your reverse proxy of choice to proxy `*.haze.example.com` and - `haze.example.com` to the proxy's listen endpoint -- (optional) acquire a wildcard ssl certificate for your domain and set your - reverse proxy to use it. This will be highly dependent on your DNS provider, - [this](https://community.letsencrypt.org/t/dns-providers-who-easily-integrate-with-lets-encrypt-dns-validation/86438) - lists some DNS providers and supported ACME clients. - -### Local Setup - -- Setup `dnsmasq` to resolve `*.haze.test` to your development machine. -- Generate a wildcard ssl certificate for `*.haze.test` using `mkcert`: - -```bash -# Generate local wildcard certificate -mkcert -cert-file haze.test.crt -key-file haze.test.key '*.haze.test' -``` - -- Set up a service to run `haze proxy` in the background as your own user. A - systemd user service is recommended (see [haze.service](./haze.service) for an - example). -- Setup a reverse proxy to proxy `*.haze.test` and `haze.test` to the - `haze proxy`'s socket. Example for Nginx: - -```nginx -upstream haze-handler { - server unix:/run/haze/haze.sock; -} - -server { - listen 80; - listen 443 ssl; - http2 on; - server_name *.haze.test; - - ssl_certificate /haze.test.crt; - ssl_certificate_key /haze.test.key; - - location / { - proxy_pass http://haze-handler; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} -``` - -### Usage - -When the proxy is configured, generated URLs for the instances will use a -subdomain of the configured domain, e.g. the `rolling-bees` instance will be -available at `rolling-bees.haze.example.com`. Additionally, `haze.example.com` -will automatically point to the last created instance. - -Additionally, the proxy allows access to the server containers trough either -`-.haze.example.com` for a specific instance, or -`.haze.example.com` for the last created instance. For example -`rolling-bees-mail.haze.example.com` will give access to the smtp4dev web -interface of the `rolling-bees` instance. - -## Haze scripts - -Haze scripts combine a set of instance options and a script to run in the -instance. - -Haze scripts are intended to way to create automated ways of running more -complex tests are setting up more complex instances. - -A script contains of 3 paths - -1. An optional shebang line setting `haze` as the interpreter, e.g. - `#! /usr/bin/env -S haze script` -2. A shebang line setting the options for the instance creation as the - interpreter, e.g. `#! haze shell pgsql s3` -3. The rest that is ran as a script inside the created instance. - -The first sheband is set, the script can be ran directly. Else it needs to be -run with `haze script [path-to-script]`. - -For example, the following script will create an instance with `postgresql` and -`s3` primary storage. Then creates a new file, gets the file id, read the object -of the file from S3, validate that it contains the expected contents, and -cleanup the instance. - -```bash -#! #! /usr/bin/env -S haze script -#! haze shell pgsql s3 - -echo 'test' | occ file:put '-' /admin/files/test.txt -FILE_ID=$(occ info:file /admin/files/test.txt | grep fileid: | grep -oE '[0-9]+') -OBJECT_CONTENTS=$(occ file:object:get urn:oid:$FILE_ID -) -if [ "$OBJECT_CONTENTS" == 'test' ]; then - echo "object contains expected contents" -else - echo "object does not contains expected contents!" -fi -``` - -### Script modes - -Scripts can be either `shell` or `start` scripts. - -`shell` scripts will automatically remove the created instance once the script -is done, just like `haze shell` will. The intended use case for this is -performing some tests in the created instance without leaving state behind. - -`start` scripts on the other hand will keep the instance, like `haze start` -will. The intended use case for this is creating more complex instances without -having to configure a dedicated preset. - -The script mode is determined based on the shebang line. The mode set in a -script can be overriden by running the script with -`haze script [shell|start] path-to-script.sh`. - -## Nushell scripts - -By default, the script contents are executed as a `bash` script, you can instead -execute the script as a `nushell` script by using `.nu` as the file extention. - -## Configuration - -Configuration is loaded from `~/.config/haze/haze.toml` and has the following -options - -```toml -sources_root = "/path/to/sources" # path of the nextcloud sources. required -app_directories = ["/path/to/sources/more_app"] # paths to additional app directories. -work_dir = "/path/to/temp/dir" # path to temporary directory. optional, defaults to "/tmp/haze" -worktree_dir = "/path/to/worktrees" # where to create the worktrees of detached instances. optional, defaults to "/worktrees" - -[auto_setup] # optional -enabled = false # whether or not to automatically install nextcloud on `haze start`. enabled by default -username = "foo" # username for admin user during auto setup. optional, defaults to "admin" -password = "bar" # password for admin user during auto setup. optional, defaults to "admin" -enable_apps = ["files_external"] # apps to enable after setup, defaults to [] -disable_apps = ["contacts"] # apps to disable after setup, defaults to [] -post_setup = [# commands to execute after setup, defaults to [] - "occ group:add test", -] -config = { "foo" = "bar" } # configuration options to set before install - -[[volume]] # optional -source = "/tmp/haze-shared" -target = "/shared" -create = true - -[[volume]] -source = "/home/me/Downloads" -target = "/Downloads" -read_only = true - -[proxy] # optional -address = "haze.example.com" # base domain -https = true # Is the proxy behind a https terminating proxy -listen = "/run/haze/haze.sock" # either a unix socket path -#listen = "127.0.0.1:8080" # or a socket address - -# presets allow for easy usage of commonly used setups -[[preset]] -name = "groupfolders" # name of the preset -apps = ["groupfolders"] # app to enable -commands = ["occ groupfolders:create gf", "occ groupfolders:group 1 admin read write share delete"] # commands to run post-setup -``` +See [DEVELOPING.md](./DEVELOPING.md) for information on how to work on `haze`. diff --git a/book/.gitignore b/book/.gitignore new file mode 100644 index 0000000..e9c0728 --- /dev/null +++ b/book/.gitignore @@ -0,0 +1 @@ +book \ No newline at end of file diff --git a/book/README.md b/book/README.md new file mode 100644 index 0000000..0f73f56 --- /dev/null +++ b/book/README.md @@ -0,0 +1,9 @@ +## Build requirements + +- [mdBook](https://github.com/rust-lang/mdBook) + +## Development + +``` +mdbook serve +``` diff --git a/book/book.toml b/book/book.toml new file mode 100644 index 0000000..c2ab188 --- /dev/null +++ b/book/book.toml @@ -0,0 +1,4 @@ +[book] +title = "Haze" +authors = ["Robin Appelman"] +language = "en" diff --git a/book/src/README.md b/book/src/README.md new file mode 100644 index 0000000..e5ee49c --- /dev/null +++ b/book/src/README.md @@ -0,0 +1,30 @@ +# Haze + +Hazy with a chance of clouds. + +`haze` is a tool that provides an easy way to set up Nextcloud test instances +with a choice of PHP version, database server, optional s3 or LDAP setup and +much more. + +## Quickstart + +- Grab a binary from the + [Codeberg releases](https://codeberg.org/icewind/haze/releases) and place it + in your `$PATH` + +- Create a file `~/.config/haze/haze.toml` with the following content: + + ```toml + sources_root = "/path/to/nextcloud/sources" + ``` + +- Start a basic Nextcloud instance: + + ```bash + haze start + ``` + +- Navigate to the address that is provided in the output. + +See the [setup](./setup.html), [configuration](./configuration.html) and +[usage](./usage.html) for a more details. diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md new file mode 100644 index 0000000..b201bc6 --- /dev/null +++ b/book/src/SUMMARY.md @@ -0,0 +1,16 @@ +# Summary + +[Introduction](README.md) + +- [Setup](setup.md) +- [Usage](usage.md) +- [Configuration](configuration.md) +- [Proxy](proxy/README.md) + - [DNS](proxy/dns.md) + - [HTTPS](proxy/https.md) +- [Services](services.md) +- [Database](database.md) +- [Federation](federation.md) +- [Haze scripts](scripts.md) +- [Xdebug](xdebug.md) +- [FrankenPHP (experimental)](frankenphp.md) diff --git a/book/src/configuration.md b/book/src/configuration.md new file mode 100644 index 0000000..8ec8d8a --- /dev/null +++ b/book/src/configuration.md @@ -0,0 +1,271 @@ +# Configuration + +Configuration is loaded from `~/.config/haze/haze.toml`. + +The minimum required configuration needed to get started is just the +`sources_root` options. + +The full list of supported options is: + +### `sources_root` + +The local path of the Nextcloud sources. This options is **required**. + +Type: string + +### `app_directories` + +A list of additional app directory paths to look for apps into + +Default `[]` + +Type: list of strings + +### `work_dir` + +The location where haze keeps it's temporary files and caches + +Default: `/tmp/haze` + +Type: string + +### `worktree_dir` + +The location to store git worktrees when using instances with detached sources. + +Default: `/worktrees` + +Type: string + +## `auto_setup` + +Options for automatically setting up the newly created Nextcloud instance. + +Examples: + +```toml +[auto_setup] +username = "foo" +password = "bar" +enable_apps = ["files_external"] +disable_apps = ["contacts"] +post_setup = [ + "occ group:add test", +] +config = { "enforce_theme" = "dark" } +``` + +```toml +[auto_setup] +enabled = false +``` + +### `auto_setup.enabled` + +Whether to automatically setup Nextcloud inside a created instance. + +Default: `true` + +Type: boolean + +### `auto_setup.username` + +The username to use for the admin account during auto setup. + +Default: `admin` + +Type: string + +### `auto_setup.password` + +The password to use for the admin account during auto setup. + +Default: `admin` + +Type: string + +### `auto_setup.enabled_apps` + +Extra apps to enable after auto setup + +Default: `[]` + +Type: list of strings + +### `auto_setup.disabled_apps` + +Apps to disabled after auto setup + +Default: `[]` + +Type: list of strings + +### `auto_setup.post_setup` + +Commands to execute after auto setup + +Default: `[]` + +Type: list of strings + +### `auto_setup.config` + +System configuration options to set before auto setup + +Default: `{}` + +Type: Object + +## `volume` + +Additional files or directories to bind-mount into instances. + +Any number of volume options can be configured + +Examples: + +```toml +[[volume]] +source = "/tmp/haze-shared" +target = "/shared" +create = true +``` + +```toml +[[volume]] +source = "/home/me/Downloads" +target = "/Downloads" +read_only = true +``` + +### `volume.source` + +The source path on the host + +Type: string + +### `volume.target` + +The target path inside the container + +Type: string + +### `volume.create` + +Create the source directory on the host if it doesn't exist already. + +Default: `false` + +Type: boolean + +### `volume.read_only` + +Whether to mount the file or directory as read only + +Default: `false` + +Type: `boolean` + +### preset + +Configured presets that can be used when creating instances. + +Any number of presets can be configured. + +Example: + +```toml +[[preset]] +name = "groupfolders" +apps = ["groupfolders"] +commands = [ + "occ groupfolders:create gf", + "occ groupfolders:group 1 admin read write share delete" +] +``` + +### `preset.name` + +The name of the preset, this is used when creating instances to select the +preset. + +Type: string without whitespace + +### `preset.apps` + +A list of apps to enable when the preset is used. + +Default: `[]` + +Type: list of strings + +### `preset.commands` + +A list of commands to run post-setup when the preset is used. + +Default: `[]` + +Type: list of strings + +## `proxy` + +Configuration for the haze proxy. Only required when using the proxy. + +```toml +[proxy] +address = "haze.example.com" +listen = "/run/haze/haze.sock" +https = true +cert = "/path/to/haze.test.crt" +key = "/path/to/haze.test.key" +``` + +### `proxy.listen` + +The IP and port or Unix socket for the proxy to listen on. + +**Required** when the proxy is enabled. + +Type: string + +Examples: + +```toml +listen = "/run/haze/haze.sock" +``` + +```toml +listen = "127.0.0.1:8080" +``` + +### `proxy.address` + +The base domain the proxy is accessible on. + +**Required** if the proxy is enabled. + +Type: string + +### `proxy.https` + +Whether to enable built-in HTTPS support for the proxy. + +Default: `false` + +Type: boolean + +### `proxy.cert` + +The path of the PEM encoded certificate chain to use for HTTPS. + +**Required** if HTTPS is enabled for the proxy. + +Type: string. + +### `proxy.cert` + +The path of the PEM encoded private key to use for HTTPS. + +**Required** if HTTPS is enabled for the proxy. + +Type: string. diff --git a/book/src/database.md b/book/src/database.md new file mode 100644 index 0000000..bf96761 --- /dev/null +++ b/book/src/database.md @@ -0,0 +1,33 @@ +# Database + +There are 3 ways of accessing the database of a haze instance. + +## CLI + +```bash +haze [filter] db [query] +``` + +Opens the command line client for the database. If a query is specified, it will +be executed. If no query is specified, an interactive shell will be started. + +Example + +``` +haze db 'select fileid, storage, path from oc_filecache' +``` + +## WebUI + +A web UI (based on [DbGate](https://www.dbgate.io/)) is available with every +instance. + +It can be accessed by using `haze open db`, or, if the proxy is setup at +`db.haze.example.com`. + +## External tools + +For usage with external tools, the database connection URL is printed when the +instance is created. + +Details on how to use the connection URL is dependent on the external tool. diff --git a/book/src/federation.md b/book/src/federation.md new file mode 100644 index 0000000..23bab7a --- /dev/null +++ b/book/src/federation.md @@ -0,0 +1,13 @@ +# Federation + +Multiple instances can reach each other by using their instance name as domain +name to allow for testing federation between instances. + +For example, if you have a `rover-beavers` and `splendid-couch` instance, you +can create a federated share from the `rover-beavers` instance to +`http://admin@splendid-couch`. + +If the proxy is setup with HTTP, you can use HTTP between the instances by using +the full proxy URLs. + +For example sharing to `admin@splendid-couch.haze.example.com`. diff --git a/book/src/frankenphp.md b/book/src/frankenphp.md new file mode 100644 index 0000000..92f675b --- /dev/null +++ b/book/src/frankenphp.md @@ -0,0 +1,11 @@ +# FrankenPHP (experimental) + +You can have Nextcloud run on FrankenPHP by starting the instance with the +`franken-php` option. + +Caddy's admin metrics are then accessible through +`http://-franken-php.haze.test`. With ember you can run: + +```bash +ember --addr http://-franken-php.haze.test +``` diff --git a/book/src/proxy/README.md b/book/src/proxy/README.md new file mode 100644 index 0000000..edc643e --- /dev/null +++ b/book/src/proxy/README.md @@ -0,0 +1,91 @@ +# Proxy + +By default, instances can be accessed by their IP. In order to get more +memorable URLs and allow supporting HTTPS. haze comes with a builtin reverse +proxy to allow using a wildcard domain. + +## Setup + +- [Setup a DNS record](./dns.html) for `*.haze.example.com` and + `haze.example.com` pointing to your development machine. +- Set the `proxy` configuration with your domain and desired listen endpoint. +- Set up a service to run `haze proxy` in the background as your own user. A + SystemD user service is recommended (see + [haze.service](<[./haze.service](https://codeberg.org/icewind/haze/src/branch/main/haze.service)>) + for an example). +- If you're already running a reverse proxy, configure your reverse proxy of + choice to proxy `*.haze.example.com` and `haze.example.com` to the proxy's + listen endpoint. +- (Optionally) [setup HTTPS](./https.html) for the proxy. + +### Configuration + +Add the following configuration to the `haze.toml` configuration file: + +```toml +[proxy] +address = "haze.example.com" # the base domain for the proxy to use +listen = "127.0.0.1:8080" # the port+ip to listen on +# listen = "/var/run/haze/haze.sock" # or a unix socket path +``` + +### Without a reverse proxy + +If you have no other http(s) servers on your development machine, you can setup +things without a reverse proxy. + +Simply configure the proxy to listen on port `80` (or `443` when using HTTPS). + +Binding to port 80 or443 as a regular user requires either giving the haze +binary the `net_bind_service` capability with +`sudo setcap cap_net_bind_service=+ep $(which haze)` (this will have to be done +every time your upgrade haze) or configure your system to allow unprivileged +users to bind on the low port numbers. Using +`sysctl net.ipv4.ip_unprivileged_port_start=80` and writing + +``` +net.ipv4.ip_unprivileged_port_start=80 +``` + +to `/etc/sysctl.d/bind.conf` to make it persistent across reboot. + +### With a reverse proxy + +If you're already have other http(s) services listening on your machine, you'll +probably want to setup a reverse proxy to allow them to all be served on your +machine. + +The setup for this will depend on your reverse proxy of the choice, the +following example configuration is for `nginx`. + +```nginx +upstream haze-handler { + server unix:/var/run/haze/haze.sock; +} + +server { + listen 80; + server_name *.haze.example.com; + + location / { + proxy_pass http://haze-handler; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +## Usage + +When the proxy is configured, generated URLs for the instances will use a +subdomain of the configured domain, e.g. the `rolling-bees` instance will be +available at `rolling-bees.haze.example.com`. Additionally, `haze.example.com` +will automatically point to the last created instance. + +Additionally, the proxy allows access to the service containers trough either +`-.haze.example.com` for a specific instance, or +`.haze.example.com` for the last created instance. For example +`rolling-bees-mail.haze.example.com` will give access to the smtp4dev web +interface of the `rolling-bees` instance. diff --git a/book/src/proxy/dns.md b/book/src/proxy/dns.md new file mode 100644 index 0000000..eec3413 --- /dev/null +++ b/book/src/proxy/dns.md @@ -0,0 +1,27 @@ +# DNS + +Since the domain name used for the instance is dynamic, a wildcard DNS record is +required. + +## With your domain's DNS provider + +If you own a domain you would like to use, you can create a wildcard domain +within the DNS settings of your DNS provider. For example creating a record an +`A` record for `*.haze.example.com` with a value of `127.0.0.1` and a similar +one for `haze.example.com`. + +## With a local dnsmasq + +If you do not own a "real" domain for using with haze, you can setup `dnsmasq` +locally to achieve the same goal instead. + +How to install and enable `dnsmasq` will depend on your Linux distribution of +choice and should be documented by it's documentation. + +Once setup, a configuration line like + +``` +address=/haze.local/172.0.0.1 +``` + +should be enough to point `haze.local` and `*.haze.local` to your local host. diff --git a/book/src/proxy/https.md b/book/src/proxy/https.md new file mode 100644 index 0000000..6369030 --- /dev/null +++ b/book/src/proxy/https.md @@ -0,0 +1,78 @@ +# HTTPS + +The proxy can be setup to enable using HTTPS to access the running instances. +Besides the warm and fuzzy feeling of knowing that nobody can snoop on the +traffic that is happening completely local inside your machine. Accessing the +page over HTTPS is required for some JavaScript features (such as service +workers), as they are only available in "secure contexts". + +## Getting a wildcard certificate + +Since the domain name used for the instance is dynamic, a wildcard certificate +is required. + +## Let's encrypt + +Let's encrypt allows getting trusted wildcard certificates for free if you can +use DNS validation. + +How to setup DNS validation will depend on the specifics of the DNS provider and +ACME client. +[This](https://community.letsencrypt.org/t/dns-providers-who-easily-integrate-with-lets-encrypt-dns-validation/86438) +lists some DNS providers and supported ACME clients. + +## Self signed + +You can also create a self-signed wildcard certificate using a tool like +`mkcert`. This certificate will not be trusted by your browser and tools like +curl, but you can add manually add it to the trusted certificates on your +system, or bypass the certificates warning in the browser/curl every time. + +```bash +# Generate local wildcard certificate +mkcert -cert-file haze.example.com.crt -key-file haze.example.com.key '*.haze.example.com' +``` + +## Using the certificate + +### Without reverse proxy + +The haze proxy can serve over HTTPS directly, to enable that add the following +to the `[proxy]` section of your `haze.toml`. + +```toml +https = true +cert = "/path/to/haze.example.com.crt" +key = "/path/to/haze.example.com.key" +``` + +You might also want to change the port it's listening on to `443`. + +### With a reverse proxy + +This depends on what reverse proxy you have setup. The following example is for +`nginx`. + +```nginx +upstream haze-handler { + server unix:/run/haze/haze.sock; +} + +server { + listen 80; + listen 443 ssl; + http2 on; + server_name *.haze.example.com; + + ssl_certificate /haze.example.com.crt; + ssl_certificate_key /haze.example.com.key; + + location / { + proxy_pass http://haze-handler; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` diff --git a/book/src/scripts.md b/book/src/scripts.md new file mode 100644 index 0000000..b32c3c8 --- /dev/null +++ b/book/src/scripts.md @@ -0,0 +1,72 @@ +# Haze scripts + +Haze scripts combine a set of instance options and a script to run in the +instance. + +Haze scripts are intended to way to create automated ways of running more +complex tests are setting up more complex instances. + +A script contains of 3 paths + +1. An optional shebang line setting `haze` as the interpreter, e.g. + `#! /usr/bin/env -S haze script` +2. A shebang line setting the options for the instance creation as the + interpreter, e.g. `#! haze shell pgsql s3` +3. The rest that is ran as a script inside the created instance. + +The first shebang is set, the script can be ran directly. Else it needs to be +run with `haze script [path-to-script]`. + +For example, the following script will create an instance with `postgresql` and +`s3` primary storage. Then creates a new file, gets the file id, read the object +of the file from S3, validate that it contains the expected contents, and +cleanup the instance. + +```bash +#! #! /usr/bin/env -S haze script +#! haze shell pgsql s3 + +echo 'test' | occ file:put '-' /admin/files/test.txt +FILE_ID=$(occ info:file /admin/files/test.txt | grep fileid: | grep -oE '[0-9]+') +OBJECT_CONTENTS=$(occ file:object:get urn:oid:$FILE_ID -) +if [ "$OBJECT_CONTENTS" == 'test' ]; then + echo "object contains expected contents" +else + echo "object does not contains expected contents!" +fi +``` + +### Script modes + +Scripts can be either `shell` or `start` scripts. + +`shell` scripts will automatically remove the created instance once the script +is done, just like `haze shell` will. The intended use case for this is +performing some tests in the created instance without leaving state behind. + +`start` scripts on the other hand will keep the instance, like `haze start` +will. The intended use case for this is creating more complex instances without +having to configure a dedicated preset. + +The script mode is determined based on the shebang line. The mode set in a +script can be overridden by running the script with +`haze script [shell|start] path-to-script.sh`. + +## Using different interpreters + +By default, the script contents are executed as either `bash` script, or `nu` +script based on the extension. + +You can overwrite the interpreter used to execute the script by adding an extra +`#!` line to the script, for example: + +```bash +#! /usr/bin/env -S haze script +#! haze shell pgsql s3 +#! php -f +`: by specifying the path to an app package this package + will be extracted into the apps. directory of the new instance (overwriting + any existing app code). This can be used to quickly test a packaged app. +- The name of any configured preset. diff --git a/book/src/setup.md b/book/src/setup.md new file mode 100644 index 0000000..420eaee --- /dev/null +++ b/book/src/setup.md @@ -0,0 +1,36 @@ +# Setup + +## Requirements + +- Docker + +haze is built around docker containers and manages containers using the docker +socket. Using Podman with docker compatibility might also work, but is untested. + +## Installation + +- Grab a binary from the + [Codeberg releases](https://codeberg.org/icewind/haze/releases) and place it + in your `$PATH` + +## Configuration + +Create a file `~/.config/haze/haze.toml` with the following options: + +```toml +sources_root = "/path/to/nextcloud/sources" +``` + +See the [configuration section](./configuration.html) for more options. + +## Test the Setup + +### Quick examples + +- Start a basic Nextcloud instance: + + ```bash + haze start + ``` + +- Navigate to the address that is provided in the output. diff --git a/book/src/usage.md b/book/src/usage.md new file mode 100644 index 0000000..063bc47 --- /dev/null +++ b/book/src/usage.md @@ -0,0 +1,230 @@ +# Usage + +## Quick examples + +- Start a Nextcloud instance with `postgresql`, and `s3` primary storage: + + ```bash + haze start pgsql s3 + ``` + +- Start a Nextcloud instance with `sqlite`, `php 8.3` and an `smb` external + storage: + + ```bash + haze start 8.3 smb + ``` + +- Run specific units test against an `oracle` database + ```bash + haze test oracle apps/dav/tests/unit/Connector/Sabre + ``` + +## Managing instances + +### Starting an instance + +```bash +haze start [--name ] [--detach] [database] [php-version] [services] [vX.Y.Z] +``` + +Where `database` is one of `sqlite`, `mysql`, `mariadb`, `pgsql` or `oracle` +with an optional version (e.g. `pgsql:12`), defaults to `sqlite`. And +`php-version` is one of `8.0`, `8.1`, `8.2`, `8.3`, `8.4` or `8.5`, defaults to +the maximum version support by the current Nextcloud version. + +You can specify a version number (e.g. `v32.0.2`) to use the sources from a +release instead of using the local sources. + +Use `--name ` to give the instance a specific name instead of a randomly +generated one. For example: + +Use `--detach` to give the instance +[its own sources](#instances-with-their-own-sources) instead of sharing +`sources_root` with all other instances. + +See the [services documentation](./services.html) for a list of available +services. + +### List running instances + +```bash +haze +``` + +or + +```bash +haze list +``` + +### Stop an instance + +```bash +haze [match] stop +``` + +### Remove all unpinned running instances + +```bash +haze clean +``` + +### Pin an instance + +```bash +haze [match] pin +``` + +Pinned instances will not be removed by `haze clean`. + +### Unpin an instance + +```bash +haze [match] unpin +``` + +## Run commands in a temporary instance + +```bash +haze shell [database] [php-version] [services] [cmd] +``` + +This will create an instance, run the provided command, and cleanup the +instance. + +If no `cmd` is specified it will launch `bash` + +## Run tests in a new instance + +```bash +haze test [database] [php-version] [services] [phpunit version] [path] +``` + +Where `path` is a file or folder to run PHPUnit in, relative to the sources +root. + +This will create a fresh instance, run the PHPUnit tests, and clean up the +created instance. + +## Interacting with running instances + +The following commands run against the most recently started instance by default +and allow optionally providing a `match` to select a specific instance by its +name. + +### Open an instance in the browser + +```bash +haze [match] open +``` + +### Execute a command on an instance + +```bash +haze [match] [service] [cmd] +``` + +If no `cmd` is specified it will launch `bash` + +If a service name or `db` is provided, the command will be in the container of +the service or database. + +If no `cmd` is specified it will launch `bash` + +### Execute an occ command on an instance + +```bash +haze [match] occ [cmd] +``` + +### Connect to the database on an instance + +```bash +haze [match] db +``` + +### Show the logs of an instance + +```bash +haze [match] logs +``` + +### Edit a file in an instance with the local $EDITOR + +```bash +haze [match] edit +``` + +Where `` is the path of a file inside the container, for example +`config/config.php`. + +### Reload the PHP configuration of an instance + +```bash +haze [match] reload +``` + +The PHP configuration can edit changed with `haze edit /config/php.ini` + +### Run a command with instance environment variables set + +```bash +haze [match] env [args] +``` + +Runs the provided command with `NEXTCLOUD_URL`, `DATABASE_URL` and `REDIS_URL` +environment variables set for the matched instance. + +This is intended to run a local +[push daemon](https://github.com/nextcloud/notify_push) against an instance. + +## Git tools + +Haze provides a couple of utilities to make working with many git repositories +for apps easier. + +### Checkout a branch for all local apps + +```bash +haze git checkout [branch] +``` + +Checks out the branch in all git repositories within the apps folder. + +Defaults to the branch matching the current checked out server versions (e.g. +`master` or `stable33`). + +`master` and `main` can be used interchangeably. + +### Pull remote changes for all local apps + +```bash +haze git pull +``` + +Performs a pull in all git repositories within the apps folder. + +## Update the container images + +```bash +haze update +``` + +## Instances with their own sources + +By default every instance shares the sources configured as `sources_root`, so +all instances always run the same code. An instance started with `--detach` gets +its own `git worktree` of `sources_root` instead, created in `worktree_dir`, +which lets you run several instances on different branches at the same time. + +```bash +haze start --name my-fix --detach +``` + +The worktree is checked out with a detached head. Every app in one of the +`app_directories` that gets enabled during setup receives its own worktree as +well, other apps keep running from the shared app directory. + +The worktrees are removed together with the instance, by `haze stop` or +`haze clean`. diff --git a/book/src/xdebug.md b/book/src/xdebug.md new file mode 100644 index 0000000..58079ca --- /dev/null +++ b/book/src/xdebug.md @@ -0,0 +1,36 @@ +# Xdebug + +To use Xdebug running in a haze instance with your IDE for debugging, you need +to tell you IDE how to properly map the path from the container to the host. The +IDE debugger configuration usually looks like: + +```json + { + "label": "PHP: Debug server within docker", + "adapter": "Xdebug", + "request": "launch", + "port": 9003, + "pathMappings": { + "/var/www/html": "", + "/var/www/html/apps-extra": "", + }, + }, +``` + +This would have to be adapted for detached instances. + +## Debugging all requests + +By default, Xdebug is configured to only run for requests that containing the +trigger (e.g. from using the Xdebug browser extension, or adding +`?XDEBUG_SESSION_START=1` to the URL). + +To enable Xdebug for all requests: + +- Un-comment the lines in `haze [cloud-id] edit /config/php.ini` +- Then run `haze reload [cloud-id]` + +## Xdebug profile and trace files + +When enabling Xdebug trace or profile mode, the generated files can be found in +`//xdebug`. diff --git a/example-script.sh b/example-script.sh index 5e8a821..410a702 100755 --- a/example-script.sh +++ b/example-script.sh @@ -1,5 +1,6 @@ #! /usr/bin/env -S haze script #! haze shell pgsql s3 +#! /usr/bin/env nu echo 'test' | occ file:put '-' /admin/files/test.txt FILE_ID=$(occ info:file /admin/files/test.txt | grep fileid: | grep -oE '[0-9]+') diff --git a/flake.lock b/flake.lock index 3b2be4f..c08fe28 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1780099841, - "narHash": "sha256-EVZd2RsbpreRUDSi9rBwPY+ZxoyMaiEBbZxxhljbaS4=", + "lastModified": 1788465171, + "narHash": "sha256-Y1/TTVXjYXGF068IThQH9fPSZ0SIE74PABlUxnWTUH0=", "owner": "ipetkov", "repo": "crane", - "rev": "0532eb17955225173906d671fb36306bdeb1e2dc", + "rev": "eb35abda9f232cc6610b1d1e3200d15c49b7ac54", "type": "github" }, "original": { @@ -38,11 +38,11 @@ ] }, "locked": { - "lastModified": 1781543995, - "narHash": "sha256-wsSyxNWOSMofu4F5xGYGMrB1d8cepvBNhxhm9bGGmXg=", + "lastModified": 1790013821, + "narHash": "sha256-0OrPYAGfGF5BGPPtzEzeMNhNcvGyQBWRRfvcE0xY750=", "owner": "nix-community", "repo": "flakelight", - "rev": "fd1d557721e07b5a9d319442e4bcb5d286600011", + "rev": "a57b0af9d0da4c5389ccf19e310daf0e5518312a", "type": "github" }, "original": { @@ -60,11 +60,11 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1780231986, - "narHash": "sha256-OyafczPtzE0Xa2zl3j/KvV2+ZVYGhYQHt0MOVWtDXlY=", + "lastModified": 1789480598, + "narHash": "sha256-G1jy1kz2dLJCkVjjY2PnCXEL1B2iqMlSfz01YFCBY+c=", "ref": "refs/heads/main", - "rev": "f5cbda29b945df03256bf63c22fa4cd5fa429e67", - "revCount": 72, + "rev": "c1e026caf1750f226a20dab66594dc1ef3a34ec4", + "revCount": 77, "type": "git", "url": "https://codeberg.org/icewind/mill-scale.git" }, @@ -80,11 +80,11 @@ ] }, "locked": { - "lastModified": 1775487831, - "narHash": "sha256-2lguQpLPQaxpQCJjXhmEEAfabwsAhkP29Z7fgLzHARA=", + "lastModified": 1790258944, + "narHash": "sha256-3bwtAiifsfGXVLIopwhJ/RNIkDABcLg6DxnlbaiE2xc=", "owner": "nlewo", "repo": "nix2container", - "rev": "76be9608a7f4d6c985d28b0e7be903ae2547df3e", + "rev": "08d8889b6d2528acfce6e2616b0cd41983dedb02", "type": "github" }, "original": { @@ -95,11 +95,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1781216227, - "narHash": "sha256-9mUW6gNwoN2SWc/l0fW4svPNOulXLl8ijqKyeSOGgJE=", + "lastModified": 1790218706, + "narHash": "sha256-6e4Na3z008XpdVyOXgfasXIn1aN+z8AXFG+jXdQSyuI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a0374025a863d007d98e3297f6aa46cc3141c2f0", + "rev": "c508844df6c28fa6dabc1b6af70f3ccbd65c5201", "type": "github" }, "original": { @@ -110,11 +110,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1781268102, - "narHash": "sha256-Zn5KTggEmUB3lXn/ccERNcBdddE6IaOFber9dWViWDg=", + "lastModified": 1789370336, + "narHash": "sha256-6RSEDHIWQtesQKWSu5qRai8L2h4KgCgMEfJHstW99G4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "49a4bd0573c376468dd7996ddb6f9fa31d8c4d97", + "rev": "c7def046b9a883d46974757852106483d741586f", "type": "github" }, "original": { @@ -127,15 +127,14 @@ "phps": { "inputs": { "flake-compat": "flake-compat", - "nixpkgs": "nixpkgs_2", - "utils": "utils" + "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1781456983, - "narHash": "sha256-z3SNuQpkSeIRTS6Y/Q5FgGAuGSY5+YJBXtqWPXw0f0k=", + "lastModified": 1790337121, + "narHash": "sha256-phqixEWMLjvdlUpo6uBCYbVYuGCk0FjiIoC0zPTCu3c=", "owner": "fossar", "repo": "nix-phps", - "rev": "6458735dece7e48f27d52ac68eee2d2cfe55b79a", + "rev": "2a7ca28d4634890d2b07a63847cd6d26da9c2cb4", "type": "github" }, "original": { @@ -162,11 +161,11 @@ ] }, "locked": { - "lastModified": 1780197589, - "narHash": "sha256-FVCr2Ij/jKf59a4LW481eeOF6rJRreOBrVgW/aUBTrw=", + "lastModified": 1789457514, + "narHash": "sha256-Aggle++fTyAifBy+QBPxjM+obO5iepKW/8MDxQtgGvI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "21632e942d89bf1cce4e5a63d7e58a215a0cbfcc", + "rev": "89e99bf0778a8f2cd18c9360c3f19c1ee47fc739", "type": "github" }, "original": { @@ -174,39 +173,6 @@ "repo": "rust-overlay", "type": "github" } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index a3b6ebd..d4c9127 100644 --- a/flake.nix +++ b/flake.nix @@ -45,14 +45,6 @@ (final: prev: { inherit (phps.packages.${prev.system}) php81 php80; inherit (nix2container.packages.x86_64-linux) nix2container; - blackfire = prev.blackfire.overrideAttrs ( - oldAttrs: finalAttrs: { - src = final.fetchurl { - url = "https://packages.blackfire.io/debian/pool/any/main/b/blackfire/blackfire_${finalAttrs.version}_amd64.deb"; - sha256 = "sha256-1fswfZEElLyXWqvNW76BpKTpBomK9cglJtitZgcpxhM="; - }; - } - ); }) ]; @@ -63,6 +55,7 @@ "haze-image-php-8.2" = pkgs: pkgs.haze-image-php-82; "haze-image-php-8.1" = pkgs: pkgs.haze-image-php-81; "haze-image-php-8.0" = pkgs: pkgs.haze-image-php-80; + haze-book = pkgs: pkgs.haze-book; }; tools = pkgs: @@ -71,8 +64,14 @@ bacon skopeo dive + mdbook + vale ]; + checks = { + vale = {vale, ...}: "${vale}/bin/vale src/ book/src/ book/README.md *.md"; + }; + homeModules = { default = { pkgs, diff --git a/nix/book.nix b/nix/book.nix new file mode 100644 index 0000000..a1b1442 --- /dev/null +++ b/nix/book.nix @@ -0,0 +1,20 @@ +{ + mdbook, + stdenv, + ... +}: +stdenv.mkDerivation { + name = "haze-book"; + src = ../book; + + nativeBuildInputs = [mdbook]; + + buildPhase = '' + mdbook build + ''; + + installPhase = '' + mkdir -p $out + cp -r book/* $out/ + ''; +} diff --git a/nix/image/bootstrap b/nix/image/bootstrap index 4e7e0f3..80a741b 100755 --- a/nix/image/bootstrap +++ b/nix/image/bootstrap @@ -2,7 +2,6 @@ touch /var/log/nginx/access.log touch /var/log/nginx/error.log -touch /var/log/cron/owncloud.log mkdir /config if not ("/config/php.ini" | path exists) { @@ -35,6 +34,7 @@ let dirs = [ let files = [ ["condition", "path"]; ["/", "/var/www/html/build/integration/composer.lock"], + ["/", "/var/log/cron/haze.log"] ] $dirs | each { |dir| @@ -88,6 +88,7 @@ if ("REDIS_TLS" in $env) { cp /etc/supervisor/redis-tls.conf /etc/supervisor/enabled/ } else { cp /etc/supervisor/redis-plain.conf /etc/supervisor/enabled/ + cp /etc/supervisor/redis-plain.conf /etc/supervisor/enabled/ } if ("BLACKFIRE_SERVER_ID" in $env) { diff --git a/nix/image/configs.nix b/nix/image/configs.nix index afa06c7..e563aa0 100644 --- a/nix/image/configs.nix +++ b/nix/image/configs.nix @@ -3,5 +3,4 @@ runCommand "configs" {} '' mkdir -p $out cp -r ${./configs} $out/etc chmod -R +w $out/etc - mkdir $out/etc/supervisor/enabled/ '' diff --git a/nix/image/configs/cron.d/nc b/nix/image/configs/cron.d/nc new file mode 100644 index 0000000..b9fb836 --- /dev/null +++ b/nix/image/configs/cron.d/nc @@ -0,0 +1,2 @@ +# m h dom mon dow user command +* * * * * haze php -f /var/www/html/cron.php -- -v >> /var/log/cron/haze.log 2>&1 diff --git a/nix/image/configs/oc-cron.conf b/nix/image/configs/oc-cron.conf deleted file mode 100644 index 81c6020..0000000 --- a/nix/image/configs/oc-cron.conf +++ /dev/null @@ -1,2 +0,0 @@ -# m h dom mon dow command -*/5 * * * * sudo -u haze php -f /var/www/html/cron.php >> /var/log/cron/haze.log 2>&1 diff --git a/nix/image/configs/supervisor/enabled/cron.conf b/nix/image/configs/supervisor/enabled/cron.conf new file mode 100644 index 0000000..9646598 --- /dev/null +++ b/nix/image/configs/supervisor/enabled/cron.conf @@ -0,0 +1,2 @@ +[program:cron] +command = crond -x sch -f diff --git a/nix/image/configs/supervisor/frankenphp.conf b/nix/image/configs/supervisor/frankenphp.conf index 0ee52b1..cc88f7d 100644 --- a/nix/image/configs/supervisor/frankenphp.conf +++ b/nix/image/configs/supervisor/frankenphp.conf @@ -1,3 +1,5 @@ [program:frankenphp] +environment = SERVER_NAME=":80",CADDY_ADMIN=":2019" command = /bin/frankenphp run directory = /var/www/html +user=haze \ No newline at end of file diff --git a/nix/image/scripts/nc-auto-config b/nix/image/scripts/nc-auto-config index 674baaa..0cf9554 100755 --- a/nix/image/scripts/nc-auto-config +++ b/nix/image/scripts/nc-auto-config @@ -2,7 +2,7 @@ touch /var/log/nginx/access.log touch /var/log/nginx/error.log -touch /var/log/cron/owncloud.log +touch /var/log/cron/haze.log if ("/var/www/html/config/config.php" | path exists) { exit 0 diff --git a/nix/overlay.nix b/nix/overlay.nix index 276b70e..030b559 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -6,4 +6,5 @@ final: prev: { haze-image-php-82 = final.callPackage ./image/haze.nix {php = final.php82;}; haze-image-php-81 = final.callPackage ./image/haze.nix {php = final.php81;}; haze-image-php-80 = final.callPackage ./image/haze.nix {php = final.php80;}; + haze-book = final.callPackage ./book.nix {}; } diff --git a/src/args.rs b/src/args.rs index 181db11..1f13cc2 100644 --- a/src/args.rs +++ b/src/args.rs @@ -64,6 +64,7 @@ pub enum HazeArgs { }, Open { filter: Option, + service: Option, }, Fmt { path: String, @@ -317,7 +318,20 @@ impl HazeArgs { .into_diagnostic()?, }) } - HazeCommand::Open => Ok(HazeArgs::Open { filter }), + HazeCommand::Open => { + let mut args = args.peekable(); + let service = match args.peek() { + Some(arg) => Service::from_type(&[], arg.as_ref()) + .into_iter() + .filter_map(|services| services.into_iter().next()) + .next() + .inspect(|_| { + args.next(); + }), + _ => None, + }; + Ok(HazeArgs::Open { filter, service }) + } HazeCommand::Fmt => { let path = args .next() @@ -472,6 +486,7 @@ pub enum HazeCommand { ))] Logs, /// Open an instance in the browser + #[strum(props(Args = "[service] service to open, instead of the Nextcloud instance"))] Open, /// Run code formatting from a new instance #[strum(props(Args = "[path] path to format"))] @@ -507,7 +522,7 @@ pub enum HazeCommand { /// Edit a file in the instance with $EDITOR on the host #[strum(props(Args = "[path] file to edit"))] Edit, - /// Reload the php configuration in the instance + /// Reload the PHP configuration in the instance #[strum(props( Details = "note: you can overwrite php.ini settings with haze [filter] edit /config/php.ini" ))] diff --git a/src/cloud.rs b/src/cloud.rs index 6932d00..515537f 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -4,9 +4,9 @@ use crate::exec::{ExitCode, exec, exec_io, exec_tty}; use crate::git::{create_worktree, find_app_repo, init_submodules, remove_worktree}; use crate::mapping::{Mapping, for_config}; use crate::php::PhpVersion; -use crate::service::Service; use crate::service::ServiceTrait; use crate::service::deduplicate_services; +use crate::service::{DbGate, Service}; use crate::sources::download_nc; use bollard::Docker; use bollard::config::NetworkCreateRequest; @@ -347,6 +347,8 @@ fn test_option_parse() { ); } +static DEFAULT_SERVICES: &[Service] = &[Service::DbGate(DbGate)]; + #[derive(Debug, Clone)] pub struct Cloud { pub id: String, @@ -539,6 +541,8 @@ impl Cloud { options .services .iter() + .filter(|service| !service.hidden()) + .chain(DEFAULT_SERVICES.iter()) .map(|service| service.spawn(docker, &id, &network, config, &options)), ) .await?; @@ -717,9 +721,9 @@ impl Cloud { .await .into_diagnostic() .wrap_err("Failed to remove work directory") - { - eprintln!("{}", e); - } + { + eprintln!("{}", e); + } remove_worktrees_for_instance(config, &self.id).await?; @@ -808,14 +812,15 @@ impl Cloud { && match filter.as_ref() { Some(filter) => cloud_id.contains(filter), None => true, - } { - let entry = containers_by_id.entry(cloud_id.to_string()).or_default(); - if labels.get("haze-type").map(String::as_str) == Some("cloud") { - entry.0 = Some(container); - } else { - entry.1.push(container) - } } + { + let entry = containers_by_id.entry(cloud_id.to_string()).or_default(); + if labels.get("haze-type").map(String::as_str) == Some("cloud") { + entry.0 = Some(container); + } else { + entry.1.push(container) + } + } } let mut sortable_containers: Vec<_> = containers_by_id @@ -969,7 +974,7 @@ impl Cloud { } pub fn services(&self) -> impl Iterator { - self.options.services.iter() + self.options.services.iter().chain(DEFAULT_SERVICES.iter()) } pub fn db(&self) -> &Database { diff --git a/src/config.rs b/src/config.rs index 392b453..64e140d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -201,10 +201,14 @@ pub struct ProxyConfig { pub address: String, #[serde(default)] pub https: bool, + #[serde(default)] + pub cert: Option, + #[serde(default)] + pub key: Option, } impl ProxyConfig { - /// Get a public address for a service, either with direct ip or through the proxy + /// Get a public address for a service, either with direct IP or through the proxy pub fn addr(&self, id: &str, ip: IpAddr) -> String { let clean_id = id.strip_prefix("haze-").unwrap_or(id); match (&self.address, self.https) { diff --git a/src/database.rs b/src/database.rs index 0bc31d7..a38da00 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,13 +1,14 @@ -use crate::exec::{exec, exec_tty, ExitCode}; +use crate::config::HazeConfig; +use crate::exec::{ExitCode, exec, exec_tty}; use crate::image::pull_image; +use bollard::Docker; use bollard::config::ContainerCreateBody; use bollard::models::{EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; -use bollard::Docker; use maplit::hashmap; use miette::{IntoDiagnostic, Report, Result, WrapErr}; -use std::io::{stdout, Stdout}; -use std::net::IpAddr; +use std::io::{Stdout, stdout}; +use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; use std::time::Duration; use strum::{Display, EnumIter, EnumProperty, IntoStaticStr}; @@ -31,6 +32,39 @@ impl DatabaseFamily { pub fn name(&self) -> &'static str { self.into() } + + pub fn db_gate_driver(&self) -> &'static str { + match self { + DatabaseFamily::Mysql => "mysql@dbgate-plugin-mysql", + DatabaseFamily::MariaDB => "mariadb@dbgate-plugin-mysql", + DatabaseFamily::Postgres => "postgres@dbgate-plugin-postgres", + DatabaseFamily::Oracle => "oracle@dbgate-plugin-oracle", + DatabaseFamily::Sqlite => "sqlite@dbgate-plugin-sqlite", + } + } + + pub fn port(&self) -> u16 { + match self { + DatabaseFamily::Mysql | DatabaseFamily::MariaDB => 3306, + DatabaseFamily::Postgres => 5432, + DatabaseFamily::Oracle => 1521, + DatabaseFamily::Sqlite => 0, + } + } + + pub fn username(&self) -> &'static str { + match self { + DatabaseFamily::Oracle => "system", + _ => "haze", + } + } + + pub fn db(&self) -> &'static str { + match self { + DatabaseFamily::Oracle => "SYSTEM", + _ => "haze", + } + } } #[derive(Clone, Debug, Eq, PartialEq, Default)] @@ -414,6 +448,37 @@ impl Database { } } + pub async fn location( + &self, + docker: &Docker, + cloud_id: &str, + config: &HazeConfig, + ) -> Result { + let ip = match self.family() { + DatabaseFamily::Mysql + | DatabaseFamily::MariaDB + | DatabaseFamily::Postgres + | DatabaseFamily::Oracle => self + .ip(docker, cloud_id) + .await + .ok_or_else(|| Report::msg("Failed to get the IP of the database container"))?, + DatabaseFamily::Sqlite => IpAddr::V4(Ipv4Addr::LOCALHOST), + }; + + match self.family() { + DatabaseFamily::Mysql | DatabaseFamily::MariaDB => { + Ok(format!("mysql://haze:haze@{ip}/haze")) + } + DatabaseFamily::Postgres => Ok(format!("postgresql://haze:haze@{ip}/haze")), + DatabaseFamily::Oracle => Ok(format!("oracle://system:haze@{ip}:1521/XE")), + DatabaseFamily::Sqlite => Ok(config + .work_dir + .join(cloud_id) + .join("data/haze.db") + .to_string()), + } + } + pub async fn is_healthy(&self, docker: &Docker, cloud_id: &str, postfix: &str) -> Result { match self.family() { DatabaseFamily::Sqlite => Ok(true), diff --git a/src/exec.rs b/src/exec.rs index f077329..f206a32 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -75,7 +75,7 @@ pub async fn exec_tty, S2: Into, Env: Into>( } }); - // set stdout in raw mode so we can do tty stuff + // set stdout in raw mode so we can do TTY stuff let mut stdout = stdout.lock().into_raw_mode().into_diagnostic()?; // pipe docker exec output into stdout diff --git a/src/help.rs b/src/help.rs index 566bff7..fec9415 100644 --- a/src/help.rs +++ b/src/help.rs @@ -151,12 +151,14 @@ fn subcommand_help(command: &dyn SubCommand) { for service in ServiceType::iter() { let service: ServiceType = service; let service_str: &'static str = service.into(); - println!( - " {}{} {}", - service.blue(), - " ".repeat(max_service_len - service_str.len()), - service.get_documentation().unwrap_or_default(), - ); + if let Some(doc) = service.get_documentation() { + println!( + " {}{} {}", + service.blue(), + " ".repeat(max_service_len - service_str.len()), + doc, + ); + } } println!(); diff --git a/src/image.rs b/src/image.rs index e24abfd..26850d9 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,6 +1,6 @@ +use bollard::Docker; use bollard::models::CreateImageInfo; use bollard::query_parameters::CreateImageOptions; -use bollard::Docker; use futures_util::StreamExt; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use miette::{IntoDiagnostic, Result, WrapErr}; diff --git a/src/main.rs b/src/main.rs index bcfd7e5..2271d5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -93,8 +93,15 @@ async fn main() -> Result { clear_networks(&docker, &retain).await?; - for cache_dir in config.work_dir.read_dir().into_diagnostic()? { - let cache_dir = cache_dir.into_diagnostic()?; + for cache_dir in config + .work_dir + .read_dir() + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read dir {}", config.work_dir))? + { + let cache_dir = cache_dir + .into_diagnostic() + .wrap_err_with(|| "Failed to unwrap cache_dir")?; if let Some(id) = cache_dir.file_name().to_str() && id.starts_with("haze-") && !retain.iter().any(|cloud| cloud.id == id) @@ -117,12 +124,16 @@ async fn main() -> Result { } } - prune_worktrees(&config)?; + prune_worktrees(&config).wrap_err_with(|| "Failed to prune worktree")?; } HazeArgs::List { filter } => { let list = Cloud::list(&docker, filter, &config).await?; for cloud in list { - let mut services: Vec<_> = cloud.services().map(Service::name).collect(); + let mut services: Vec<_> = cloud + .services() + .filter(|service| !service.hidden()) + .map(Service::name) + .collect(); services.push(cloud.db().name()); let services = services.join(", "); let pin = if cloud.is_pinned(&docker).await.unwrap_or(false) { @@ -275,11 +286,26 @@ async fn main() -> Result { .await?; } } - HazeArgs::Open { filter } => { + HazeArgs::Open { filter, service } => { let cloud = Cloud::get_by_filter(&docker, filter, &config).await?; - match cloud.ip { - Some(_) => opener::open(cloud.address).into_diagnostic()?, - None => eprintln!("{} is not running", cloud.id), + match service { + Some(service) => { + let Some(ip) = service.get_ips(&docker, &cloud.id).await?.next() else { + eprintln!( + "Service {} can't be opened as it has no associated IP", + service.name() + ); + return Ok(ExitCode::FAILURE); + }; + let addr = config + .proxy + .addr(&format!("{}-{}", cloud.id, service.name()), ip); + opener::open(addr).into_diagnostic()?; + } + None => match cloud.ip { + Some(_) => opener::open(cloud.address).into_diagnostic()?, + None => eprintln!("{} is not running", cloud.id), + }, } } HazeArgs::Test { options, mut args } => { @@ -422,21 +448,15 @@ async fn main() -> Result { let ip = cloud .ip .ok_or_else(|| Report::msg(format!("{} is not running", cloud.id)))?; - let db_type = match cloud.db().family() { + match cloud.db().family() { DatabaseFamily::Sqlite => { return Err(Report::msg("sqlite is not supported with `haze env`")); } DatabaseFamily::Oracle => { return Err(Report::msg("oracle is not supported with `haze env`")); } - DatabaseFamily::Mysql | DatabaseFamily::MariaDB => "mysql", - DatabaseFamily::Postgres => "postgresql", - }; - let db_ip = cloud - .db() - .ip(&docker, &cloud.id) - .await - .ok_or_else(|| Report::msg(format!("{}-db is not running", cloud.id)))?; + _ => {} + } let mut command = Command::new(command); command @@ -445,7 +465,7 @@ async fn main() -> Result { .env("NEXTCLOUD_URL", &cloud.address) .env( "DATABASE_URL", - format!("{}://haze:haze@{}/haze", db_type, db_ip), + cloud.db().location(&docker, &cloud.id, &config).await?, ); if cloud.services().contains(&Service::RedisTls(RedisTls)) { @@ -519,16 +539,7 @@ async fn main() -> Result { &docker, &cloud.id, "root", - vec!["pkill", "php-fpm"], - Vec::::new(), - Some(stdout()), - ) - .await?; - exec( - &docker, - &cloud.id, - "root", - vec!["sh", "-c", "php-fpm --fpm-config /etc/php-fpm.conf&"], + vec!["supervisorctl", "restart", "php-fpm"], Vec::::new(), Some(stdout()), ) @@ -552,6 +563,16 @@ async fn setup( let cloud = Cloud::create(docker, options, config).await?; println!("{}", cloud.address); let host = cloud.address.split_once("://").expect("no address?").1; + + match cloud.db().location(docker, &cloud.id, config).await { + Ok(value) => { + println!("Database: {}", value); + } + Err(e) => { + println!("Failed to print DB location: {}", e); + } + } + if always_setup || config.auto_setup.enabled { println!("Waiting for servers to start"); cloud.wait_for_start(docker).await?; @@ -614,6 +635,20 @@ async fn setup( ) .await?; } + cloud + .occ( + docker, + vec![ + "config:app:set", + "core", + "backgroundjobs_mode", + "--value", + "cron", + ], + None, + Vec::::default(), + ) + .await?; let domains = [ip_str.as_str(), "cloud", &cloud.id, host]; for (i, domain) in domains.iter().enumerate() { diff --git a/src/mapping.rs b/src/mapping.rs index 6291c53..30b82ef 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -107,7 +107,7 @@ impl<'a> Mapping<'a> { .wrap_err("Failed to create source file")?, } - // For worktrees, pre-create mountpoints within /var/www/html to prevent docker from creating them as root. + // For worktrees, pre-create mount points within /var/www/html to prevent docker from creating them as root. if self.target.starts_with("/var/www/html") { let local_target = source_root.join( self.target diff --git a/src/network.rs b/src/network.rs index 386e13c..1ea2050 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,6 +1,6 @@ use crate::cloud::Cloud; -use bollard::config::NetworkCreateRequest; use bollard::Docker; +use bollard::config::NetworkCreateRequest; use miette::{IntoDiagnostic, Result, WrapErr}; pub async fn clear_networks(docker: &Docker, instances: &[Cloud]) -> Result<()> { @@ -12,9 +12,10 @@ pub async fn clear_networks(docker: &Docker, instances: &[Cloud]) -> Result<()> for network in networks { if let Some(name) = network.name.as_deref() && let Some(id) = name.strip_prefix("haze-") - && !instances.iter().any(|cloud| cloud.id == id) { - docker.remove_network(name).await.ok(); - } + && !instances.iter().any(|cloud| cloud.id == id) + { + docker.remove_network(name).await.ok(); + } } Ok(()) } diff --git a/src/php.rs b/src/php.rs index bb4764c..e56267a 100644 --- a/src/php.rs +++ b/src/php.rs @@ -1,13 +1,13 @@ use crate::config::ProxyConfig; use crate::database::Database; -use crate::image::{image_version, pull_image, ImageVersion}; +use crate::image::{ImageVersion, image_version, pull_image}; use crate::network::ensure_network_exists; use crate::service::Service; use crate::service::ServiceTrait; +use bollard::Docker; use bollard::config::{ContainerCreateBody, NetworkConnectRequest, NetworkingConfig}; use bollard::models::{EndpointSettings, HostConfig}; use bollard::query_parameters::CreateContainerOptions; -use bollard::Docker; use itertools::Itertools; use maplit::hashmap; use miette::{IntoDiagnostic, Report, Result, WrapErr}; @@ -71,7 +71,7 @@ impl PhpVersion { PhpVersion::Php82 => "8.2", PhpVersion::Php83 => "8.3", PhpVersion::Php84 => "8.4", - PhpVersion::Php85 => "8.4", + PhpVersion::Php85 => "8.5", } } @@ -127,15 +127,16 @@ impl PhpVersion { let image_version = image_version(docker, self.image()).await; let haze_version = ImageVersion::from_str(env!("CARGO_PKG_VERSION")); if let (Some(image_version), Ok(haze_version)) = (image_version, haze_version) - && image_version < haze_version { - eprintln!( - "{}: image version is out of date, run {} to update.", - "Warning".red(), - "haze update".blue() - ); - eprintln!(" Haze version: {}", haze_version.bright_yellow()); - eprintln!(" Image version: {}", image_version.bright_yellow()); - } + && image_version < haze_version + { + eprintln!( + "{}: image version is out of date, run {} to update.", + "Warning".red(), + "haze update".blue() + ); + eprintln!(" Haze version: {}", haze_version.bright_yellow()); + eprintln!(" Image version: {}", image_version.bright_yellow()); + } let options = Some(CreateContainerOptions { name: Some(id.to_string()), diff --git a/src/proxy.rs b/src/proxy.rs index ffc6e4c..cc6f6ea 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1,8 +1,8 @@ -use crate::service::{ServiceTrait, ServiceType}; use crate::Result; +use crate::service::{ServiceTrait, ServiceType}; use crate::{Cloud, HazeConfig}; -use axum::http::header::HOST; use axum::http::HeaderValue; +use axum::http::header::HOST; use axum::{ body::Body, extract::Request, @@ -10,13 +10,13 @@ use axum::{ }; use bollard::Docker; use futures_util::StreamExt; +use hyper::StatusCode; use hyper::body::Incoming; use hyper::server::conn::http1; use hyper::service::service_fn; -use hyper::StatusCode; use hyper_util::rt::TokioIo; use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; -use miette::{miette, IntoDiagnostic}; +use miette::{IntoDiagnostic, miette}; use std::collections::HashMap; use std::convert::Infallible; use std::fs::{create_dir_all, set_permissions}; @@ -32,6 +32,10 @@ use tokio::net::UnixListener; use tokio::signal::ctrl_c; use tokio::spawn; use tokio::time::sleep; +use tokio_rustls::TlsAcceptor; +use tokio_rustls::rustls::ServerConfig; +use tokio_rustls::rustls::pki_types::pem::PemObject; +use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; use tokio_stream::wrappers::{TcpListenerStream, UnixListenerStream}; use tracing::{debug, error, info}; @@ -147,9 +151,34 @@ pub async fn proxy(docker: Docker, config: HazeConfig) -> Result<()> { } let listen = config.proxy.listen.clone(); + let acceptor = match (&config.proxy.cert, &config.proxy.key) { + (None, None) => None, + (Some(_), None) => return Err(miette!("`cert` is set without `key`")), + (None, Some(_)) => return Err(miette!("`key` is set without `cert`")), + (Some(cert), Some(key)) => Some(tls_acceptor(cert, key)?), + }; + let base_address = config.proxy.address.clone(); let instances = ActiveInstances::new(docker, config); - serve(instances, listen, base_address).await + serve(instances, listen, base_address, acceptor).await +} + +/// Build a TLS acceptor from a PEM encoded certificate chain and private key on disk +fn tls_acceptor(cert: &str, key: &str) -> Result { + let certs = CertificateDer::pem_file_iter(cert) + .map_err(|e| miette!("failed to load certificate from {cert}: {e}"))? + .collect::, _>>() + .map_err(|e| miette!("failed to load certificate from {cert}: {e}"))?; + let key = PrivateKeyDer::from_pem_file(key) + .map_err(|e| miette!("failed to load private key from {key}: {e}"))?; + + let mut server_config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key) + .into_diagnostic()?; + server_config.alpn_protocols = vec![b"http/1.1".to_vec()]; + + Ok(TlsAcceptor::from(Arc::new(server_config))) } #[derive(Clone)] @@ -159,7 +188,12 @@ struct AppState { proxy_client: Arc, } -async fn serve(instances: ActiveInstances, listen: String, base_address: String) -> Result<()> { +async fn serve( + instances: ActiveInstances, + listen: String, + base_address: String, + acceptor: Option, +) -> Result<()> { let instances = Arc::new(instances); let base_address = Arc::new(base_address); let last_instances = instances.clone(); @@ -188,12 +222,13 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String) if !listen.starts_with('/') { let addr: SocketAddr = listen.parse().into_diagnostic()?; let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - println!("listening on {}", listener.local_addr().unwrap()); + let scheme = if acceptor.is_some() { "https" } else { "http" }; + println!("Listening on {scheme}://{}", listener.local_addr().unwrap()); let mut connections = pin!(TcpListenerStream::new(listener).take_until(cancel)); while let Some(stream) = connections.next().await { match stream { - Ok(stream) => handle_connection(state.clone(), stream), + Ok(stream) => handle_connection(state.clone(), stream, acceptor.clone()).await, Err(error) => { error!(%error, "connection failed"); } @@ -202,10 +237,11 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String) } else { let listen: PathBuf = listen.into(); if let Some(parent) = listen.parent() - && !parent.exists() { - create_dir_all(parent).into_diagnostic()?; - set_permissions(parent, PermissionsExt::from_mode(0o755)).into_diagnostic()?; - } + && !parent.exists() + { + create_dir_all(parent).into_diagnostic()?; + set_permissions(parent, PermissionsExt::from_mode(0o755)).into_diagnostic()?; + } let _ = tokio::fs::remove_file(&listen).await; let listener = UnixListener::bind(&listen).unwrap(); @@ -216,7 +252,7 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String) while let Some(stream) = connections.next().await { match stream { - Ok(stream) => handle_connection(state.clone(), stream), + Ok(stream) => handle_connection(state.clone(), stream, None).await, Err(error) => { error!(%error, "connection failed"); } @@ -227,7 +263,22 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String) Ok(()) } -fn handle_connection( +async fn handle_connection( + state: AppState, + stream: I, + acceptor: Option, +) { + // Spawn a tokio task to serve multiple connections concurrently + match acceptor { + Some(acceptor) => match acceptor.accept(stream).await { + Ok(stream) => serve_connection(state, stream).await, + Err(error) => error!(%error, "tls handshake failed"), + }, + None => serve_connection(state, stream).await, + } +} + +async fn serve_connection( state: AppState, stream: I, ) { @@ -287,7 +338,7 @@ async fn handler(state: AppState, mut req: Request) -> Result shell, + (None, Some("nu")) => vec!["nu".into()], + _ => vec!["bash".into()], }; options.mappings.push( @@ -61,7 +63,10 @@ pub async fn run_script( cloud .exec( docker, - vec![shell.to_string(), target_path.into_string()], + shell + .into_iter() + .chain(once(target_path.into_string())) + .collect(), true, get_forward_env(), ) @@ -74,14 +79,22 @@ pub async fn run_script( Ok(()) } -fn parse_script(script: &str, config: &HazeConfig) -> Result<(CloudOptions, ScriptMode)> { - let (options, mode) = script +#[derive(Debug)] +struct Script { + options: CloudOptions, + mode: ScriptMode, + shell: Option>, +} + +fn parse_script(script: &str, config: &HazeConfig) -> Result