From 185968224a6f15a647ed1a4db4ab58bb6c5a6ffc Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 15:26:29 +0200 Subject: [PATCH 01/19] feat: add TLS configuration for proxy service in workspace.yml --- home/service/proxy/traefik/root/config/tls.yaml | 6 ------ home/workspace.yml | 3 +++ 2 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 home/service/proxy/traefik/root/config/tls.yaml diff --git a/home/service/proxy/traefik/root/config/tls.yaml b/home/service/proxy/traefik/root/config/tls.yaml deleted file mode 100644 index 25eb10bb..00000000 --- a/home/service/proxy/traefik/root/config/tls.yaml +++ /dev/null @@ -1,6 +0,0 @@ -tls: - stores: - default: - defaultCertificate: - certFile: /tls/my127.site.crt - keyFile: /tls/my127.site.key diff --git a/home/workspace.yml b/home/workspace.yml index 1a53940b..84a58bed 100644 --- a/home/workspace.yml +++ b/home/workspace.yml @@ -18,6 +18,9 @@ attributes.default: global: service: proxy: + domain: my127.site https: crt: https://my127.io/workspace/my127.site.crt key: https://my127.io/workspace/my127.site.key + crt_file: = @('global.service.proxy.domain') ~ '.crt' + key_file: = @('global.service.proxy.domain') ~ '.key' From c660f3df16ad2a320d690870eab3258d90fa8e93 Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 15:29:20 +0200 Subject: [PATCH 02/19] feat: update proxy service configurations to use dynamic domain variables --- config/workspace/global.yml | 30 ++++++++++++++++++------- home/service/logger/docker-compose.yml | 2 +- home/service/mail/docker-compose.yml | 2 +- home/service/proxy/docker-compose.yml | 2 +- home/service/tracing/docker-compose.yml | 2 +- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/config/workspace/global.yml b/config/workspace/global.yml index a1d7f55b..8485c7fd 100644 --- a/config/workspace/global.yml +++ b/config/workspace/global.yml @@ -1,23 +1,37 @@ -command('global service logger (enable|disable)', 'global service logger'): | - #!bash|= - ws-service logger ={input.command(4)} +command('global service logger (enable|disable)', 'global service logger'): + env: + MY127WS_PROXY_DOMAIN: = @('global.service.proxy.domain') + exec: | + #!bash|= + ws-service logger ={input.command(4)} command('global service mail (enable|disable)', 'global service mail'): - description: Actions for mailhog (mail.my127.site) + description: Actions for mailhog + env: + MY127WS_PROXY_DOMAIN: = @('global.service.proxy.domain') exec: | #!bash|= ws-service mail ={input.command(4)} command('global service proxy (enable|disable|restart)', 'global service proxy'): - description: Actions for Traefik proxy (my127.site) + description: Actions for Traefik proxy + env: + MY127WS_PROXY_DOMAIN: = @('global.service.proxy.domain') + MY127WS_PROXY_HTTPS_CRT: = @('global.service.proxy.https.crt') + MY127WS_PROXY_HTTPS_KEY: = @('global.service.proxy.https.key') + MY127WS_PROXY_HTTPS_CRT_FILE: = @('global.service.proxy.https.crt_file') + MY127WS_PROXY_HTTPS_KEY_FILE: = @('global.service.proxy.https.key_file') exec: | #!bash|= ws-service proxy ={input.command(4)} -command('global service tracing (start|stop|restart)', 'global service tracing'): | - #!bash|= - ws-service tracing ={input.command(4)} +command('global service tracing (start|stop|restart)', 'global service tracing'): + env: + MY127WS_PROXY_DOMAIN: = @('global.service.proxy.domain') + exec: | + #!bash|= + ws-service tracing ={input.command(4)} command('global service', 'global service'): description: Perform an action (start/stop etc.) on a global service diff --git a/home/service/logger/docker-compose.yml b/home/service/logger/docker-compose.yml index 798c6d2d..b707eeab 100644 --- a/home/service/logger/docker-compose.yml +++ b/home/service/logger/docker-compose.yml @@ -6,7 +6,7 @@ services: - elasticsearch labels: - traefik.enable=true - - traefik.http.routers.kibana.rule=Host(`kibana.my127.site`) + - traefik.http.routers.kibana.rule=Host(`kibana.${MY127WS_PROXY_DOMAIN:-my127.site}`) - traefik.http.services.kibana.loadbalancer.server.port=5601 - traefik.docker.network=${TRAEFIK_NETWORK} networks: diff --git a/home/service/mail/docker-compose.yml b/home/service/mail/docker-compose.yml index 2d6edef9..41618f65 100644 --- a/home/service/mail/docker-compose.yml +++ b/home/service/mail/docker-compose.yml @@ -4,7 +4,7 @@ services: restart: unless-stopped labels: - traefik.enable=true - - traefik.http.routers.mail.rule=Host(`mail.my127.site`) + - traefik.http.routers.mail.rule=Host(`mail.${MY127WS_PROXY_DOMAIN:-my127.site}`) - traefik.http.services.mail.loadbalancer.server.port=8025 networks: - private diff --git a/home/service/proxy/docker-compose.yml b/home/service/proxy/docker-compose.yml index 7cf24807..b450b1b0 100644 --- a/home/service/proxy/docker-compose.yml +++ b/home/service/proxy/docker-compose.yml @@ -9,7 +9,7 @@ services: - 443:443/udp labels: - traefik.enable=true - - traefik.http.routers.traefik.rule=Host(`my127.site`) + - traefik.http.routers.traefik.rule=Host(`${MY127WS_PROXY_DOMAIN:-my127.site}`) - traefik.http.services.traefik.loadbalancer.server.port=8080 - co.elastic.logs/module=traefik volumes: diff --git a/home/service/tracing/docker-compose.yml b/home/service/tracing/docker-compose.yml index 75785e1f..d988882a 100644 --- a/home/service/tracing/docker-compose.yml +++ b/home/service/tracing/docker-compose.yml @@ -9,7 +9,7 @@ services: JAEGER_AGENT_HOST: 127.0.0.1 labels: - traefik.enable=true - - traefik.http.routers.my127ws-tracing-jaeger.rule=Host(`tracing.my127.site`) + - traefik.http.routers.my127ws-tracing-jaeger.rule=Host(`tracing.${MY127WS_PROXY_DOMAIN:-my127.site}`) - traefik.http.services.my127ws-tracing-jaeger.loadbalancer.server.port=16686 - traefik.docker.network=${TRAEFIK_NETWORK} restart: unless-stopped From 830868ee818a8a05e365760e1af358e69e2cdebe Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 16:42:04 +0200 Subject: [PATCH 03/19] feat: simplify enable function in init scripts and add TLS configuration for proxy service --- home/service/logger/init.sh | 8 ++------ home/service/mail/init.sh | 8 ++------ home/service/proxy/init.sh | 27 ++++++++++++++++++++++----- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/home/service/logger/init.sh b/home/service/logger/init.sh index c1b91aec..14ae1bf3 100755 --- a/home/service/logger/init.sh +++ b/home/service/logger/init.sh @@ -21,12 +21,8 @@ main() enable() { - if [ ! -f .flag-built ]; then - run docker-compose -p my127ws-logger up -d --build - touch .flag-built - else - run docker-compose -p my127ws-logger start - fi + run docker-compose -p my127ws-logger up -d --build + touch .flag-built } disable() diff --git a/home/service/mail/init.sh b/home/service/mail/init.sh index 1e10f2f2..fb0312c1 100755 --- a/home/service/mail/init.sh +++ b/home/service/mail/init.sh @@ -21,12 +21,8 @@ main() enable() { - if [ ! -f .flag-built ]; then - run docker-compose -p my127ws-mail up -d --build - touch .flag-built - else - run docker-compose -p my127ws-mail start - fi + run docker-compose -p my127ws-mail up -d --build + touch .flag-built } disable() diff --git a/home/service/proxy/init.sh b/home/service/proxy/init.sh index 53b80c89..df475b0f 100755 --- a/home/service/proxy/init.sh +++ b/home/service/proxy/init.sh @@ -29,13 +29,18 @@ enable() cd "$DIR" if ! docker ps | grep my127ws-proxy > /dev/null; then + MY127WS_PROXY_DOMAIN="$(ws global config get global.service.proxy.domain)" + MY127WS_PROXY_HTTPS_CRT="$(ws global config get global.service.proxy.https.crt)" + MY127WS_PROXY_HTTPS_KEY="$(ws global config get global.service.proxy.https.key)" + MY127WS_PROXY_HTTPS_CRT_FILE="$(ws global config get global.service.proxy.https.crt_file)" + MY127WS_PROXY_HTTPS_KEY_FILE="$(ws global config get global.service.proxy.https.key_file)" + export MY127WS_PROXY_DOMAIN - if [ ! -d "traefik/root/tls" ]; then - run mkdir -p traefik/root/tls - fi + run mkdir -p traefik/root/tls traefik/root/config - run curl --fail --location --output traefik/root/tls/my127.site.crt "$(ws global config get global.service.proxy.https.crt)" - run curl --fail --location --output traefik/root/tls/my127.site.key "$(ws global config get global.service.proxy.https.key)" + run curl --fail --location --output "traefik/root/tls/${MY127WS_PROXY_HTTPS_CRT_FILE}" "${MY127WS_PROXY_HTTPS_CRT}" + run curl --fail --location --output "traefik/root/tls/${MY127WS_PROXY_HTTPS_KEY_FILE}" "${MY127WS_PROXY_HTTPS_KEY}" + write_tls_config "${MY127WS_PROXY_HTTPS_CRT_FILE}" "${MY127WS_PROXY_HTTPS_KEY_FILE}" run docker-compose -p my127ws-proxy up --force-recreate --build -d traefik fi ) @@ -55,6 +60,18 @@ restart() enable } +write_tls_config() +{ + cat > traefik/root/config/tls.yaml < Date: Wed, 17 Jun 2026 16:42:14 +0200 Subject: [PATCH 04/19] feat: add tests for global proxy domain configuration and overrides --- .../GlobalProxyConfigurationTest.php | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/Test/Application/GlobalProxyConfigurationTest.php diff --git a/tests/Test/Application/GlobalProxyConfigurationTest.php b/tests/Test/Application/GlobalProxyConfigurationTest.php new file mode 100644 index 00000000..14788145 --- /dev/null +++ b/tests/Test/Application/GlobalProxyConfigurationTest.php @@ -0,0 +1,70 @@ +isolatedHomeEnvironment(); + + self::assertSame("my127.site\n", $this->workspaceCommand('global config get global.service.proxy.domain', null, $env)->getOutput()); + self::assertSame("https://my127.io/workspace/my127.site.crt\n", $this->workspaceCommand('global config get global.service.proxy.https.crt', null, $env)->getOutput()); + self::assertSame("https://my127.io/workspace/my127.site.key\n", $this->workspaceCommand('global config get global.service.proxy.https.key', null, $env)->getOutput()); + self::assertSame("my127.site.crt\n", $this->workspaceCommand('global config get global.service.proxy.https.crt_file', null, $env)->getOutput()); + self::assertSame("my127.site.key\n", $this->workspaceCommand('global config get global.service.proxy.https.key_file', null, $env)->getOutput()); + } + + public function testProxyDomainCanBeOverriddenFromGlobalConfigFile(): void + { + $env = $this->isolatedHomeEnvironment(<<<'YAML' +attribute('global.service.proxy.domain'): dev.example.test +attribute('global.service.proxy.https.crt'): https://certs.example.test/dev.example.test.crt +attribute('global.service.proxy.https.key'): https://certs.example.test/dev.example.test.key +YAML + ); + + self::assertSame("dev.example.test\n", $this->workspaceCommand('global config get global.service.proxy.domain', null, $env)->getOutput()); + self::assertSame("https://certs.example.test/dev.example.test.crt\n", $this->workspaceCommand('global config get global.service.proxy.https.crt', null, $env)->getOutput()); + self::assertSame("https://certs.example.test/dev.example.test.key\n", $this->workspaceCommand('global config get global.service.proxy.https.key', null, $env)->getOutput()); + self::assertSame("dev.example.test.crt\n", $this->workspaceCommand('global config get global.service.proxy.https.crt_file', null, $env)->getOutput()); + self::assertSame("dev.example.test.key\n", $this->workspaceCommand('global config get global.service.proxy.https.key_file', null, $env)->getOutput()); + } + + public function testProxyCertificateFilenamesCanBeOverriddenFromGlobalConfigFile(): void + { + $env = $this->isolatedHomeEnvironment(<<<'YAML' +attribute('global.service.proxy.domain'): dev.example.test +attribute('global.service.proxy.https.crt_file'): proxy.crt +attribute('global.service.proxy.https.key_file'): proxy.key +YAML + ); + + self::assertSame("proxy.crt\n", $this->workspaceCommand('global config get global.service.proxy.https.crt_file', null, $env)->getOutput()); + self::assertSame("proxy.key\n", $this->workspaceCommand('global config get global.service.proxy.https.key_file', null, $env)->getOutput()); + } + + public function testGlobalServiceComposeFilesUseConfiguredProxyDomainVariable(): void + { + $root = dirname(__DIR__, 3); + + self::assertStringContainsString('Host(`${MY127WS_PROXY_DOMAIN:-my127.site}`)', file_get_contents($root . '/home/service/proxy/docker-compose.yml')); + self::assertStringContainsString('Host(`mail.${MY127WS_PROXY_DOMAIN:-my127.site}`)', file_get_contents($root . '/home/service/mail/docker-compose.yml')); + self::assertStringContainsString('Host(`kibana.${MY127WS_PROXY_DOMAIN:-my127.site}`)', file_get_contents($root . '/home/service/logger/docker-compose.yml')); + self::assertStringContainsString('Host(`tracing.${MY127WS_PROXY_DOMAIN:-my127.site}`)', file_get_contents($root . '/home/service/tracing/docker-compose.yml')); + self::assertFileDoesNotExist($root . '/home/service/proxy/traefik/root/config/tls.yaml'); + } + + private function isolatedHomeEnvironment(?string $globalConfig = null): array + { + $home = $this->workspace()->path('home'); + + if ($globalConfig !== null) { + $this->workspace()->put('home/.config/my127/workspace/proxy.yml', $globalConfig); + } + + return ['MY127WS_HOME' => $home]; + } +} From ee3ff1a8664299a11ead6465880f72e8df8536ac Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 16:47:51 +0200 Subject: [PATCH 05/19] feat: add documentation for custom global proxy domain configuration --- docs/cheatsheet.md | 22 +++- docs/custom-proxy-domain.md | 231 ++++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 docs/custom-proxy-domain.md diff --git a/docs/cheatsheet.md b/docs/cheatsheet.md index f7061466..903a6adf 100644 --- a/docs/cheatsheet.md +++ b/docs/cheatsheet.md @@ -10,6 +10,24 @@ You need only to restart the Traefik proxy service: ws global service proxy restart ``` +## Custom proxy domain + +See [Custom Global Proxy Domain](custom-proxy-domain.md) for the full setup. + +Minimal global config file: + +```text +~/.config/my127/workspace/proxy.yml +``` + +```yaml +attribute('global.service.proxy.domain'): dev.example.test +attribute('global.service.proxy.https.crt'): https://example.test/dev.example.test.crt +attribute('global.service.proxy.https.key'): https://example.test/dev.example.test.key +``` + +Restart the proxy after changing these values. + ## How-to receive email The email service is not running by default. It can be started with: @@ -18,6 +36,6 @@ The email service is not running by default. It can be started with: ws global service mail enable ``` -This will allow email to be viewed at `https://mail.my127.site/` +This will allow email to be viewed at `https://mail./` -This will collect email sent from any server through native `sendmail`. \ No newline at end of file +This will collect email sent from any server through native `sendmail`. diff --git a/docs/custom-proxy-domain.md b/docs/custom-proxy-domain.md new file mode 100644 index 00000000..10658e99 --- /dev/null +++ b/docs/custom-proxy-domain.md @@ -0,0 +1,231 @@ +# Custom Global Proxy Domain + +Workspace uses `my127.site` by default for local HTTPS hostnames. You can +replace that single Global Proxy domain with another domain by adding a global +Workspace config file on the developer machine. + +This feature supports one proxy domain at a time. It does not register multiple +domains. + +## Contents + +- [How it fits together](#how-it-fits-together) +- [Create the proxy config file](#create-the-proxy-config-file) +- [Host the certificate files](#host-the-certificate-files) +- [Configure DNS](#configure-dns) +- [Apply the change](#apply-the-change) +- [Use the domain in a project](#use-the-domain-in-a-project) +- [Renew certificates](#renew-certificates) + +## How it fits together + +Workspace automatically loads global config files from: + +```text +~/.config/my127/workspace/*.yml +``` + +Use `proxy.yml` for the proxy override: + +```text +~/.config/my127/workspace/proxy.yml +``` + +`proxy.yml` is a normal Workspace global config file. It is not fetched from a +remote URL and it is not managed by a Workspace import command. + +```mermaid +flowchart LR + configFile["~/.config/my127/workspace/proxy.yml"] + attributes["global.service.proxy.* attributes"] + commands["ws global service proxy restart"] + init["home/service/proxy/init.sh"] + compose["docker-compose labels"] + tlsFiles["traefik/root/tls/"] + tlsConfig["traefik/root/config/tls.yaml"] + traefik["Single Traefik Global Proxy"] + + configFile --> attributes + attributes --> commands + commands --> init + init --> compose + init --> tlsFiles + init --> tlsConfig + compose --> traefik + tlsFiles --> traefik + tlsConfig --> traefik +``` + +The configured values are passed to Docker Compose as environment variables at +runtime. The source of truth remains the global Workspace config file. + +## Create the proxy config file + +Minimal `~/.config/my127/workspace/proxy.yml`: + +```yaml +attribute('global.service.proxy.domain'): dev.example.test +attribute('global.service.proxy.https.crt'): https://proxy-config.example.internal/certs/dev.example.test/fullchain.pem +attribute('global.service.proxy.https.key'): https://proxy-config.example.internal/certs/dev.example.test/privkey.pem +``` + +The local certificate filenames default to: + +```text +.crt +.key +``` + +For the example above, Workspace writes: + +```text +traefik/root/tls/dev.example.test.crt +traefik/root/tls/dev.example.test.key +``` + +Override the local filenames only when needed: + +```yaml +attribute('global.service.proxy.https.crt_file'): proxy.crt +attribute('global.service.proxy.https.key_file'): proxy.key +``` + +Full example: + +```yaml +attribute('global.service.proxy.domain'): dev.example.test +attribute('global.service.proxy.https.crt'): https://proxy-config.example.internal/certs/dev.example.test/fullchain.pem +attribute('global.service.proxy.https.key'): https://proxy-config.example.internal/certs/dev.example.test/privkey.pem +attribute('global.service.proxy.https.crt_file'): dev.example.test.crt +attribute('global.service.proxy.https.key_file'): dev.example.test.key +``` + +## Host the certificate files + +The certificate and key URLs must be reachable from each developer machine when +`ws global service proxy restart` runs. + +The files do not need to live beside `proxy.yml`. `proxy.yml` only stores the +URLs where Workspace can download them. + +Suggested hosted structure: + +```text +proxy-config/ +└── certs/ + └── dev.example.test/ + ├── fullchain.pem + └── privkey.pem +``` + +GitHub raw URLs work only for public repositories because Workspace does not +authenticate to GitHub. Use public GitHub raw URLs only for disposable test +certificates. + +Example public GitHub repository structure: + +```text +workspace-proxy-certs/ +└── certs/ + └── dev.example.test/ + ├── fullchain.pem + └── privkey.pem +``` + +Example `proxy.yml` using GitHub raw URLs: + +```yaml +attribute('global.service.proxy.domain'): dev.example.test +attribute('global.service.proxy.https.crt'): https://raw.githubusercontent.com/my-org/workspace-proxy-certs/main/certs/dev.example.test/fullchain.pem +attribute('global.service.proxy.https.key'): https://raw.githubusercontent.com/my-org/workspace-proxy-certs/main/certs/dev.example.test/privkey.pem +``` + +For organisation/private certificates, use an internal HTTP(S) location that is +reachable from developer machines, such as a private website available on the +company network or VPN. + +The certificate must cover the configured domain and the subdomains used by +projects and global services. For `dev.example.test`, the certificate should +cover: + +```text +dev.example.test +*.dev.example.test +``` + +The wildcard covers project hosts and global service hosts such as: + +```text +mail.dev.example.test +kibana.dev.example.test +tracing.dev.example.test +``` + +## Configure DNS + +The configured domain and wildcard subdomains must resolve to the developer +machine running the Workspace Global Proxy. + +For local development, common options are: + +- public DNS records that point the domain and wildcard to `127.0.0.1`; +- private DNS records available only on the organisation network; +- local DNS tools such as Pi-hole, dnsmasq, or `/etc/hosts` for individual + hostnames. + +Wildcard support is recommended because project hostnames are usually generated +under the proxy domain. + +## Apply the change + +After creating or changing `proxy.yml`, restart the proxy: + +```bash +ws global service proxy restart +``` + +The restart downloads the certificate and key, renders Traefik TLS config, and +recreates the proxy container. + +If mail, logger, or tracing are already running, restart those services too so +their Docker labels use the new hostnames: + +```bash +ws global service mail enable +ws global service logger enable +ws global service tracing restart +``` + +## Use the domain in a project + +Projects should use the same proxy domain suffix configured globally. For +example: + +```yaml +attributes: + domain: dev.example.test +``` + +Project hostnames should then be under that suffix, such as: + +```text +my-project.dev.example.test +``` + +This simplified proxy configuration supports one certificate/domain set at a +time. If the proxy is configured for `dev.example.test`, projects still using +`my127.site` may no longer match the active certificate. + +## Renew certificates + +When a certificate is renewed, keep the hosted certificate URLs stable and +replace the file contents at those URLs. + +Then run: + +```bash +ws global service proxy restart +``` + +You only need to edit `proxy.yml` when the domain, certificate URL, key URL, or +local filename changes. From 17dd727503de94ff985664ac9203c2970f95e683 Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 16:59:36 +0200 Subject: [PATCH 06/19] feat: remove unused environment variables from proxy service command and update documentation for certificate handling --- config/workspace/global.yml | 6 ------ docs/custom-proxy-domain.md | 7 ++++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/config/workspace/global.yml b/config/workspace/global.yml index 8485c7fd..273b652c 100644 --- a/config/workspace/global.yml +++ b/config/workspace/global.yml @@ -16,12 +16,6 @@ command('global service mail (enable|disable)', 'global service mail'): command('global service proxy (enable|disable|restart)', 'global service proxy'): description: Actions for Traefik proxy - env: - MY127WS_PROXY_DOMAIN: = @('global.service.proxy.domain') - MY127WS_PROXY_HTTPS_CRT: = @('global.service.proxy.https.crt') - MY127WS_PROXY_HTTPS_KEY: = @('global.service.proxy.https.key') - MY127WS_PROXY_HTTPS_CRT_FILE: = @('global.service.proxy.https.crt_file') - MY127WS_PROXY_HTTPS_KEY_FILE: = @('global.service.proxy.https.key_file') exec: | #!bash|= ws-service proxy ={input.command(4)} diff --git a/docs/custom-proxy-domain.md b/docs/custom-proxy-domain.md index 10658e99..530d9979 100644 --- a/docs/custom-proxy-domain.md +++ b/docs/custom-proxy-domain.md @@ -56,8 +56,9 @@ flowchart LR tlsConfig --> traefik ``` -The configured values are passed to Docker Compose as environment variables at -runtime. The source of truth remains the global Workspace config file. +The proxy init script reads these attributes from global Workspace config, +downloads the configured certificate files, and exports the domain for Docker +Compose labels. The source of truth remains the global Workspace config file. ## Create the proxy config file @@ -120,7 +121,7 @@ proxy-config/ GitHub raw URLs work only for public repositories because Workspace does not authenticate to GitHub. Use public GitHub raw URLs only for disposable test -certificates. +certificates. Do not publish a real private key in a public repository. Example public GitHub repository structure: From f3fa4b442fe317129b17dff5506cf8eb67a539a9 Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 17:03:03 +0200 Subject: [PATCH 07/19] feat: add descriptions for logger and tracing service commands in global configuration --- config/workspace/global.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/workspace/global.yml b/config/workspace/global.yml index 273b652c..ef09c39a 100644 --- a/config/workspace/global.yml +++ b/config/workspace/global.yml @@ -1,5 +1,6 @@ command('global service logger (enable|disable)', 'global service logger'): + description: Actions for the logging service env: MY127WS_PROXY_DOMAIN: = @('global.service.proxy.domain') exec: | @@ -21,6 +22,7 @@ command('global service proxy (enable|disable|restart)', 'global service proxy') ws-service proxy ={input.command(4)} command('global service tracing (start|stop|restart)', 'global service tracing'): + description: Actions for the tracing service env: MY127WS_PROXY_DOMAIN: = @('global.service.proxy.domain') exec: | From bd2ecb25ffcb6f760db0b6834e63564ee933e939 Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 17:06:50 +0200 Subject: [PATCH 08/19] feat: enhance create command with description and exec block formatting --- home/workspace.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/home/workspace.yml b/home/workspace.yml index 84a58bed..a8c1df53 100644 --- a/home/workspace.yml +++ b/home/workspace.yml @@ -3,16 +3,18 @@ workspace('global'): description: Manage and create your various workspaces -command('create [ [--no-install] ]', 'create'): | - #!php(cwd:/) - $name = $input->argument('name'); - $harness = $input->argument('harness'); +command('create [ [--no-install] ]', 'create'): + description: Create a new workspace, optionally using a harness + exec: | + #!php(cwd:/) + $name = $input->argument('name'); + $harness = $input->argument('harness'); - $ws->create($name, $harness); + $ws->create($name, $harness); - if ($input->option('no-install') !== true) { - $ws->passthru("cd $name; ws install"); - } + if ($input->option('no-install') !== true) { + $ws->passthru("cd $name; ws install"); + } attributes.default: global: From 29bcfa4500e8c6602ef97b7b8ad0a0dcbe38df12 Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 17:13:13 +0200 Subject: [PATCH 09/19] feat: update attribute source handling to use latest source and highest numeric precedence --- src/Types/Attribute/Collection.php | 6 ++++-- tests/Test/Types/AttributeTest.php | 32 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Types/Attribute/Collection.php b/src/Types/Attribute/Collection.php index d62b049f..1ac4d367 100644 --- a/src/Types/Attribute/Collection.php +++ b/src/Types/Attribute/Collection.php @@ -47,10 +47,12 @@ function (&$value) { $value['source'], function (&$source) { // de-dupe when attribute defined twice in same file with attribute('...') and yaml - $source = is_array($source) ? $source[0] : $source; + $source = is_array($source) ? end($source) : $source; } ); - ksort($value['source']); + uksort($value['source'], function ($a, $b) { + return (int) substr($a, 1) <=> (int) substr($b, 1); + }); } ); } diff --git a/tests/Test/Types/AttributeTest.php b/tests/Test/Types/AttributeTest.php index 10dbb89f..f81ee732 100644 --- a/tests/Test/Types/AttributeTest.php +++ b/tests/Test/Types/AttributeTest.php @@ -214,4 +214,36 @@ public function duplicateAttributeDefinitionDoesNotCauseErrorInConfigDump() $this->assertFalse(strpos($this->workspaceCommand('config dump --key=message')->getOutput(), 'World')); } + + /** @test */ + public function attributeMetadataSourceUsesLatestSourceAtTheSamePrecedence() + { + $attributes = new AttributeCollection(new Expression(new CWD())); + + $attributes->add(['message' => 'first'], 'first.yml', 1); + $attributes->add(['message' => 'second'], 'second.yml', 1); + + $metadata = $attributes->getAttributeMetadata('message'); + $this->assertNotNull($metadata); + + $sources = $metadata['source']; + $this->assertEquals('second.yml', array_pop($sources)); + $this->assertEquals('second', $attributes->get('message')); + } + + /** @test */ + public function attributeMetadataSourceUsesHighestNumericPrecedence() + { + $attributes = new AttributeCollection(new Expression(new CWD())); + + $attributes->add(['message' => 'normal'], 'normal.yml', 6); + $attributes->add(['message' => 'environment'], 'environment.yml', 10); + + $metadata = $attributes->getAttributeMetadata('message'); + $this->assertNotNull($metadata); + + $sources = $metadata['source']; + $this->assertEquals('environment.yml', array_pop($sources)); + $this->assertEquals('environment', $attributes->get('message')); + } } From 97804ec186fe7d97b4e0948221da3d5ed30cbc17 Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 17:13:19 +0200 Subject: [PATCH 10/19] feat: update offsetGet method to return mixed type and add test for array attribute access --- src/Types/Workspace/Workspace.php | 2 +- tests/Test/Types/FunctionTest.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Types/Workspace/Workspace.php b/src/Types/Workspace/Workspace.php index 090de90d..e50576ca 100644 --- a/src/Types/Workspace/Workspace.php +++ b/src/Types/Workspace/Workspace.php @@ -141,7 +141,7 @@ public function offsetExists($offset): bool return isset($this->attributes[$offset]); } - public function offsetGet($offset): ?string + public function offsetGet($offset): mixed { return $this->attributes->get($offset); } diff --git a/tests/Test/Types/FunctionTest.php b/tests/Test/Types/FunctionTest.php index c9e4f038..ee5d0692 100644 --- a/tests/Test/Types/FunctionTest.php +++ b/tests/Test/Types/FunctionTest.php @@ -116,4 +116,21 @@ function('array', [v1, v2]): | $this->assertEquals('["2","2"]', $this->workspaceCommand('array 2 2')->getOutput()); } + + /** @test */ + public function workspaceArrayAccessCanReturnArrayAttributes() + { + $this->createWorkspaceYml(<<<'EOD' +attribute('values'): + - one + - two + +command('values'): | + #!php + echo json_encode($ws['values']); +EOD + ); + + $this->assertEquals('["one","two"]', $this->workspaceCommand('values')->getOutput()); + } } From 6582f601760be6d7895531ac283c687d9441543a Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 17:57:30 +0200 Subject: [PATCH 11/19] feat: add MY127WS_PROXY_DOMAIN export in service init scripts and test for mail service path --- config/workspace/global.yml | 6 --- home/service/logger/init.sh | 3 ++ home/service/mail/init.sh | 3 ++ home/service/tracing/init.sh | 3 ++ .../GlobalProxyConfigurationTest.php | 39 +++++++++++++++++++ 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/config/workspace/global.yml b/config/workspace/global.yml index ef09c39a..4e9149f3 100644 --- a/config/workspace/global.yml +++ b/config/workspace/global.yml @@ -1,16 +1,12 @@ command('global service logger (enable|disable)', 'global service logger'): description: Actions for the logging service - env: - MY127WS_PROXY_DOMAIN: = @('global.service.proxy.domain') exec: | #!bash|= ws-service logger ={input.command(4)} command('global service mail (enable|disable)', 'global service mail'): description: Actions for mailhog - env: - MY127WS_PROXY_DOMAIN: = @('global.service.proxy.domain') exec: | #!bash|= ws-service mail ={input.command(4)} @@ -23,8 +19,6 @@ command('global service proxy (enable|disable|restart)', 'global service proxy') command('global service tracing (start|stop|restart)', 'global service tracing'): description: Actions for the tracing service - env: - MY127WS_PROXY_DOMAIN: = @('global.service.proxy.domain') exec: | #!bash|= ws-service tracing ={input.command(4)} diff --git a/home/service/logger/init.sh b/home/service/logger/init.sh index 14ae1bf3..d9752cc1 100755 --- a/home/service/logger/init.sh +++ b/home/service/logger/init.sh @@ -21,6 +21,9 @@ main() enable() { + MY127WS_PROXY_DOMAIN="$(ws global config get global.service.proxy.domain)" + export MY127WS_PROXY_DOMAIN + run docker-compose -p my127ws-logger up -d --build touch .flag-built } diff --git a/home/service/mail/init.sh b/home/service/mail/init.sh index fb0312c1..0674547d 100755 --- a/home/service/mail/init.sh +++ b/home/service/mail/init.sh @@ -21,6 +21,9 @@ main() enable() { + MY127WS_PROXY_DOMAIN="$(ws global config get global.service.proxy.domain)" + export MY127WS_PROXY_DOMAIN + run docker-compose -p my127ws-mail up -d --build touch .flag-built } diff --git a/home/service/tracing/init.sh b/home/service/tracing/init.sh index 0b6ac962..cef21e16 100755 --- a/home/service/tracing/init.sh +++ b/home/service/tracing/init.sh @@ -28,6 +28,9 @@ start() ( cd "$DIR" + MY127WS_PROXY_DOMAIN="$(ws global config get global.service.proxy.domain)" + export MY127WS_PROXY_DOMAIN + run docker-compose -p my127ws-tracing pull run docker-compose -p my127ws-tracing up -d diff --git a/tests/Test/Application/GlobalProxyConfigurationTest.php b/tests/Test/Application/GlobalProxyConfigurationTest.php index 14788145..c4f678f5 100644 --- a/tests/Test/Application/GlobalProxyConfigurationTest.php +++ b/tests/Test/Application/GlobalProxyConfigurationTest.php @@ -57,6 +57,45 @@ public function testGlobalServiceComposeFilesUseConfiguredProxyDomainVariable(): self::assertFileDoesNotExist($root . '/home/service/proxy/traefik/root/config/tls.yaml'); } + public function testDirectMailServicePathExportsConfiguredProxyDomain(): void + { + $env = $this->isolatedHomeEnvironment(<<<'YAML' +attribute('global.service.proxy.domain'): dev.example.test +YAML + ); + $this->workspaceCommand('', null, $env); + + $root = dirname(__DIR__, 3); + $fakeBin = $this->workspace()->path('fake-bin'); + mkdir($fakeBin); + symlink($root . '/bin/workspace', $fakeBin . '/ws'); + $this->workspace()->put('fake-bin/docker', <<<'BASH' +#!/bin/bash +exit 0 +BASH + ); + $this->workspace()->put('fake-bin/docker-compose', <<<'BASH' +#!/bin/bash +echo "$MY127WS_PROXY_DOMAIN" > "$MY127WS_TEST_OUTPUT" +BASH + ); + chmod($this->workspace()->path('fake-bin/docker'), 0755); + chmod($this->workspace()->path('fake-bin/docker-compose'), 0755); + $env['PATH'] = $fakeBin . ':' . getenv('PATH'); + $env['MY127WS_TEST_OUTPUT'] = $this->workspace()->path('proxy-domain-output'); + + $this->workspace()->put('workspace.yml', <<<'YAML' +command('direct service mail enable'): | + #!bash + ws-service mail enable +YAML + ); + + $this->workspaceCommand('direct service mail enable', null, $env); + + self::assertSame("dev.example.test\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); + } + private function isolatedHomeEnvironment(?string $globalConfig = null): array { $home = $this->workspace()->path('home'); From 0241a4e333535bae4a8fd9cf6bfb6a99f37a352b Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 19:10:50 +0200 Subject: [PATCH 12/19] feat: add tracing service environment file to files-bin in box.json --- box.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/box.json b/box.json index cedfecac..50ddbc10 100644 --- a/box.json +++ b/box.json @@ -10,7 +10,8 @@ "files-bin": [ "home/service/logger/.env", "home/service/mail/.env", - "home/service/proxy/.env" + "home/service/proxy/.env", + "home/service/tracing/.env" ], "blacklist": [ "tools/scripts/compile.sh", From e13be5afea89d1bfaae7480565d9a6d8337a7f45 Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 19:54:00 +0200 Subject: [PATCH 13/19] feat: enhance global proxy configuration with environment variable support and update documentation --- config/workspace/global.yml | 12 ++ docs/custom-proxy-domain.md | 120 +++++++++++++++++- home/service/logger/init.sh | 2 +- home/service/mail/init.sh | 2 +- home/service/proxy/init.sh | 10 +- home/service/tracing/init.sh | 2 +- .../GlobalProxyConfigurationTest.php | 84 ++++++++++++ 7 files changed, 217 insertions(+), 15 deletions(-) diff --git a/config/workspace/global.yml b/config/workspace/global.yml index 4e9149f3..4132d77e 100644 --- a/config/workspace/global.yml +++ b/config/workspace/global.yml @@ -1,24 +1,36 @@ command('global service logger (enable|disable)', 'global service logger'): description: Actions for the logging service + env: + MY127WS_PROXY_DOMAIN: "= env('MY127WS_PROXY_DOMAIN') ?: @('global.service.proxy.domain')" exec: | #!bash|= ws-service logger ={input.command(4)} command('global service mail (enable|disable)', 'global service mail'): description: Actions for mailhog + env: + MY127WS_PROXY_DOMAIN: "= env('MY127WS_PROXY_DOMAIN') ?: @('global.service.proxy.domain')" exec: | #!bash|= ws-service mail ={input.command(4)} command('global service proxy (enable|disable|restart)', 'global service proxy'): description: Actions for Traefik proxy + env: + MY127WS_PROXY_DOMAIN: "= env('MY127WS_PROXY_DOMAIN') ?: @('global.service.proxy.domain')" + MY127WS_PROXY_HTTPS_CRT: "= env('MY127WS_PROXY_HTTPS_CRT') ?: @('global.service.proxy.https.crt')" + MY127WS_PROXY_HTTPS_KEY: "= env('MY127WS_PROXY_HTTPS_KEY') ?: @('global.service.proxy.https.key')" + MY127WS_PROXY_HTTPS_CRT_FILE: "= env('MY127WS_PROXY_HTTPS_CRT_FILE') ?: @('global.service.proxy.https.crt_file')" + MY127WS_PROXY_HTTPS_KEY_FILE: "= env('MY127WS_PROXY_HTTPS_KEY_FILE') ?: @('global.service.proxy.https.key_file')" exec: | #!bash|= ws-service proxy ={input.command(4)} command('global service tracing (start|stop|restart)', 'global service tracing'): description: Actions for the tracing service + env: + MY127WS_PROXY_DOMAIN: "= env('MY127WS_PROXY_DOMAIN') ?: @('global.service.proxy.domain')" exec: | #!bash|= ws-service tracing ={input.command(4)} diff --git a/docs/custom-proxy-domain.md b/docs/custom-proxy-domain.md index 530d9979..606c0b11 100644 --- a/docs/custom-proxy-domain.md +++ b/docs/custom-proxy-domain.md @@ -14,7 +14,9 @@ domains. - [Host the certificate files](#host-the-certificate-files) - [Configure DNS](#configure-dns) - [Apply the change](#apply-the-change) +- [Revert to the default domain](#revert-to-the-default-domain) - [Use the domain in a project](#use-the-domain-in-a-project) +- [Switch the proxy per project](#switch-the-proxy-per-project) - [Renew certificates](#renew-certificates) ## How it fits together @@ -179,7 +181,14 @@ under the proxy domain. ## Apply the change -After creating or changing `proxy.yml`, restart the proxy: +After creating or changing `proxy.yml`, verify that Workspace resolves the new +domain: + +```bash +ws global config get global.service.proxy.domain +``` + +Then restart the proxy: ```bash ws global service proxy restart @@ -188,19 +197,67 @@ ws global service proxy restart The restart downloads the certificate and key, renders Traefik TLS config, and recreates the proxy container. -If mail, logger, or tracing are already running, restart those services too so -their Docker labels use the new hostnames: +Restart only the optional global services that the project or harness requires. +These commands can start services, so skip any service the project does not use: + +```bash +# Only if the project/harness requires mail. +ws global service mail enable + +# Only if the project/harness requires logger. +ws global service logger enable + +# Only if the project/harness requires tracing. +ws global service tracing restart +``` + +## Revert to the default domain + +Workspace supports one Global Proxy domain at a time. If another project needs +the default `my127.site` domain again, remove or rename the user-global proxy +override: + +```bash +mv ~/.config/my127/workspace/proxy.yml ~/.config/my127/workspace/proxy.yml.disabled +``` + +Verify that Workspace resolves the default domain: + +```bash +ws global config get global.service.proxy.domain +``` + +Expected output: + +```text +my127.site +``` + +Then recreate the proxy: + +```bash +ws global service proxy restart +``` + +Restart only the optional global services that the project or harness requires +so their Docker labels are regenerated with `my127.site`: ```bash +# Only if the project/harness requires mail. ws global service mail enable + +# Only if the project/harness requires logger. ws global service logger enable + +# Only if the project/harness requires tracing. ws global service tracing restart ``` ## Use the domain in a project -Projects should use the same proxy domain suffix configured globally. For -example: +Most Workspace harnesses define `domain: my127.site` in their harness +attributes. For those projects, override the project `domain` attribute to match +the active Global Proxy domain: ```yaml attributes: @@ -213,9 +270,58 @@ Project hostnames should then be under that suffix, such as: my-project.dev.example.test ``` +For custom harnesses or manually maintained Compose files, update whichever +hostname configuration the project uses. Workspace only changes the Global Proxy +domain and certificate; it does not rewrite project hostnames automatically. + This simplified proxy configuration supports one certificate/domain set at a -time. If the proxy is configured for `dev.example.test`, projects still using -`my127.site` may no longer match the active certificate. +time, so switch the global proxy before working on a project that uses a +different suffix. + +## Switch the proxy per project + +The Global Proxy still runs one domain at a time, but `ws global service ...` +commands resolve proxy settings before calling the service scripts. This allows +temporary overrides from either shell environment variables or the current +project configuration. + +Precedence: + +```text +exported shell environment +project/global Workspace attributes +installed Workspace defaults +``` + +For a project-specific proxy profile, set the proxy attributes in that project's +`workspace.override.yml`: + +```yaml +attribute('global.service.proxy.domain'): dev.example.test +attribute('global.service.proxy.https.crt'): https://proxy-config.example.internal/certs/dev.example.test/fullchain.pem +attribute('global.service.proxy.https.key'): https://proxy-config.example.internal/certs/dev.example.test/privkey.pem +``` + +Then restart the machine-global proxy from that project: + +```bash +cd path/to/project +ws global service proxy restart +``` + +For a one-off shell override, export or inline all matching proxy values before +running the restart: + +```bash +MY127WS_PROXY_DOMAIN=dev.example.test \ +MY127WS_PROXY_HTTPS_CRT=https://proxy-config.example.internal/certs/dev.example.test/fullchain.pem \ +MY127WS_PROXY_HTTPS_KEY=https://proxy-config.example.internal/certs/dev.example.test/privkey.pem \ +ws global service proxy restart +``` + +Inline environment values are useful for temporary testing. Project attributes +are easier to repeat when regularly switching between company and personal +proxy domains. ## Renew certificates diff --git a/home/service/logger/init.sh b/home/service/logger/init.sh index d9752cc1..21d297f9 100755 --- a/home/service/logger/init.sh +++ b/home/service/logger/init.sh @@ -21,7 +21,7 @@ main() enable() { - MY127WS_PROXY_DOMAIN="$(ws global config get global.service.proxy.domain)" + MY127WS_PROXY_DOMAIN="${MY127WS_PROXY_DOMAIN:-$(ws global config get global.service.proxy.domain)}" export MY127WS_PROXY_DOMAIN run docker-compose -p my127ws-logger up -d --build diff --git a/home/service/mail/init.sh b/home/service/mail/init.sh index 0674547d..e134361a 100755 --- a/home/service/mail/init.sh +++ b/home/service/mail/init.sh @@ -21,7 +21,7 @@ main() enable() { - MY127WS_PROXY_DOMAIN="$(ws global config get global.service.proxy.domain)" + MY127WS_PROXY_DOMAIN="${MY127WS_PROXY_DOMAIN:-$(ws global config get global.service.proxy.domain)}" export MY127WS_PROXY_DOMAIN run docker-compose -p my127ws-mail up -d --build diff --git a/home/service/proxy/init.sh b/home/service/proxy/init.sh index df475b0f..26753493 100755 --- a/home/service/proxy/init.sh +++ b/home/service/proxy/init.sh @@ -29,11 +29,11 @@ enable() cd "$DIR" if ! docker ps | grep my127ws-proxy > /dev/null; then - MY127WS_PROXY_DOMAIN="$(ws global config get global.service.proxy.domain)" - MY127WS_PROXY_HTTPS_CRT="$(ws global config get global.service.proxy.https.crt)" - MY127WS_PROXY_HTTPS_KEY="$(ws global config get global.service.proxy.https.key)" - MY127WS_PROXY_HTTPS_CRT_FILE="$(ws global config get global.service.proxy.https.crt_file)" - MY127WS_PROXY_HTTPS_KEY_FILE="$(ws global config get global.service.proxy.https.key_file)" + MY127WS_PROXY_DOMAIN="${MY127WS_PROXY_DOMAIN:-$(ws global config get global.service.proxy.domain)}" + MY127WS_PROXY_HTTPS_CRT="${MY127WS_PROXY_HTTPS_CRT:-$(ws global config get global.service.proxy.https.crt)}" + MY127WS_PROXY_HTTPS_KEY="${MY127WS_PROXY_HTTPS_KEY:-$(ws global config get global.service.proxy.https.key)}" + MY127WS_PROXY_HTTPS_CRT_FILE="${MY127WS_PROXY_HTTPS_CRT_FILE:-$(ws global config get global.service.proxy.https.crt_file)}" + MY127WS_PROXY_HTTPS_KEY_FILE="${MY127WS_PROXY_HTTPS_KEY_FILE:-$(ws global config get global.service.proxy.https.key_file)}" export MY127WS_PROXY_DOMAIN run mkdir -p traefik/root/tls traefik/root/config diff --git a/home/service/tracing/init.sh b/home/service/tracing/init.sh index cef21e16..727714d2 100755 --- a/home/service/tracing/init.sh +++ b/home/service/tracing/init.sh @@ -28,7 +28,7 @@ start() ( cd "$DIR" - MY127WS_PROXY_DOMAIN="$(ws global config get global.service.proxy.domain)" + MY127WS_PROXY_DOMAIN="${MY127WS_PROXY_DOMAIN:-$(ws global config get global.service.proxy.domain)}" export MY127WS_PROXY_DOMAIN run docker-compose -p my127ws-tracing pull diff --git a/tests/Test/Application/GlobalProxyConfigurationTest.php b/tests/Test/Application/GlobalProxyConfigurationTest.php index c4f678f5..80f8c0d0 100644 --- a/tests/Test/Application/GlobalProxyConfigurationTest.php +++ b/tests/Test/Application/GlobalProxyConfigurationTest.php @@ -96,6 +96,73 @@ public function testDirectMailServicePathExportsConfiguredProxyDomain(): void self::assertSame("dev.example.test\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); } + public function testGlobalProxyServiceCommandPassesProjectProxyConfiguration(): void + { + $env = $this->isolatedHomeEnvironment(); + $this->workspaceCommand('', null, $env); + + $env['MY127WS_TEST_OUTPUT'] = $this->workspace()->path('proxy-service-output'); + $this->writeFakeWsServiceRecorder(); + + $this->workspace()->put('workspace.yml', <<<'YAML' +attribute('global.service.proxy.domain'): project.example.test +attribute('global.service.proxy.https.crt'): https://certs.example.test/project.crt +attribute('global.service.proxy.https.key'): https://certs.example.test/project.key +attribute('global.service.proxy.https.crt_file'): project.crt +attribute('global.service.proxy.https.key_file'): project.key +YAML + ); + + $this->workspaceCommand('global service proxy restart', null, $env); + + $expected = <<<'TEXT' +proxy restart +project.example.test +https://certs.example.test/project.crt +https://certs.example.test/project.key +project.crt +project.key +TEXT + ; + self::assertSame($expected . "\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); + } + + public function testGlobalProxyServiceCommandAllowsShellEnvironmentOverride(): void + { + $env = $this->isolatedHomeEnvironment(); + $this->workspaceCommand('', null, $env); + + $env['MY127WS_TEST_OUTPUT'] = $this->workspace()->path('proxy-service-output'); + $env['MY127WS_PROXY_DOMAIN'] = 'shell.example.test'; + $env['MY127WS_PROXY_HTTPS_CRT'] = 'https://certs.example.test/shell.crt'; + $env['MY127WS_PROXY_HTTPS_KEY'] = 'https://certs.example.test/shell.key'; + $env['MY127WS_PROXY_HTTPS_CRT_FILE'] = 'shell.crt'; + $env['MY127WS_PROXY_HTTPS_KEY_FILE'] = 'shell.key'; + $this->writeFakeWsServiceRecorder(); + + $this->workspace()->put('workspace.yml', <<<'YAML' +attribute('global.service.proxy.domain'): project.example.test +attribute('global.service.proxy.https.crt'): https://certs.example.test/project.crt +attribute('global.service.proxy.https.key'): https://certs.example.test/project.key +attribute('global.service.proxy.https.crt_file'): project.crt +attribute('global.service.proxy.https.key_file'): project.key +YAML + ); + + $this->workspaceCommand('global service proxy restart', null, $env); + + $expected = <<<'TEXT' +proxy restart +shell.example.test +https://certs.example.test/shell.crt +https://certs.example.test/shell.key +shell.crt +shell.key +TEXT + ; + self::assertSame($expected . "\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); + } + private function isolatedHomeEnvironment(?string $globalConfig = null): array { $home = $this->workspace()->path('home'); @@ -106,4 +173,21 @@ private function isolatedHomeEnvironment(?string $globalConfig = null): array return ['MY127WS_HOME' => $home]; } + + private function writeFakeWsServiceRecorder(): void + { + $this->workspace()->put('home/.my127/workspace/bin/ws-service', <<<'BASH' +#!/bin/bash +{ + printf '%s %s\n' "$1" "$2" + printf '%s\n' "$MY127WS_PROXY_DOMAIN" + printf '%s\n' "$MY127WS_PROXY_HTTPS_CRT" + printf '%s\n' "$MY127WS_PROXY_HTTPS_KEY" + printf '%s\n' "$MY127WS_PROXY_HTTPS_CRT_FILE" + printf '%s\n' "$MY127WS_PROXY_HTTPS_KEY_FILE" +} > "$MY127WS_TEST_OUTPUT" +BASH + ); + chmod($this->workspace()->path('home/.my127/workspace/bin/ws-service'), 0755); + } } From 9a4bf4310f7ec8a62124ce1f3e2d686ad978fe62 Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 20:18:47 +0200 Subject: [PATCH 14/19] feat: add TLS validation functions to ensure certificate and key filenames are valid and distinct --- home/service/proxy/init.sh | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/home/service/proxy/init.sh b/home/service/proxy/init.sh index 26753493..74425eb9 100755 --- a/home/service/proxy/init.sh +++ b/home/service/proxy/init.sh @@ -36,6 +36,8 @@ enable() MY127WS_PROXY_HTTPS_KEY_FILE="${MY127WS_PROXY_HTTPS_KEY_FILE:-$(ws global config get global.service.proxy.https.key_file)}" export MY127WS_PROXY_DOMAIN + validate_tls_filenames "$MY127WS_PROXY_HTTPS_CRT_FILE" "$MY127WS_PROXY_HTTPS_KEY_FILE" + run mkdir -p traefik/root/tls traefik/root/config run curl --fail --location --output "traefik/root/tls/${MY127WS_PROXY_HTTPS_CRT_FILE}" "${MY127WS_PROXY_HTTPS_CRT}" @@ -60,6 +62,29 @@ restart() enable } +validate_tls_filenames() +{ + validate_tls_filename "$1" + validate_tls_filename "$2" + + if [ "$1" = "$2" ]; then + echo "TLS certificate and key filenames must be different." >&2 + exit 1 + fi +} + +validate_tls_filename() +{ + case "$1" in + ""|.|..|*/*) + echo "Invalid TLS filename: $1" >&2 + exit 1 + ;; + *) + ;; + esac +} + write_tls_config() { cat > traefik/root/config/tls.yaml < Date: Wed, 17 Jun 2026 20:18:52 +0200 Subject: [PATCH 15/19] feat: enhance global proxy service tests with TLS validation and error handling --- .../GlobalProxyConfigurationTest.php | 204 +++++++++++++++--- 1 file changed, 171 insertions(+), 33 deletions(-) diff --git a/tests/Test/Application/GlobalProxyConfigurationTest.php b/tests/Test/Application/GlobalProxyConfigurationTest.php index 80f8c0d0..9c0dc792 100644 --- a/tests/Test/Application/GlobalProxyConfigurationTest.php +++ b/tests/Test/Application/GlobalProxyConfigurationTest.php @@ -65,23 +65,39 @@ public function testDirectMailServicePathExportsConfiguredProxyDomain(): void ); $this->workspaceCommand('', null, $env); - $root = dirname(__DIR__, 3); - $fakeBin = $this->workspace()->path('fake-bin'); - mkdir($fakeBin); - symlink($root . '/bin/workspace', $fakeBin . '/ws'); - $this->workspace()->put('fake-bin/docker', <<<'BASH' + $this->prepareFakeServiceTools($env, <<<'BASH' #!/bin/bash -exit 0 +echo "$MY127WS_PROXY_DOMAIN" > "$MY127WS_TEST_OUTPUT" BASH ); - $this->workspace()->put('fake-bin/docker-compose', <<<'BASH' + $env['MY127WS_TEST_OUTPUT'] = $this->workspace()->path('proxy-domain-output'); + + $this->workspace()->put('workspace.yml', <<<'YAML' +command('direct service mail enable'): | + #!bash + ws-service mail enable +YAML + ); + + $this->workspaceCommand('direct service mail enable', null, $env); + + self::assertSame("dev.example.test\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); + } + + public function testDirectMailServicePathPreservesShellProxyDomain(): void + { + $env = $this->isolatedHomeEnvironment(<<<'YAML' +attribute('global.service.proxy.domain'): dev.example.test +YAML + ); + $this->workspaceCommand('', null, $env); + + $this->prepareFakeServiceTools($env, <<<'BASH' #!/bin/bash echo "$MY127WS_PROXY_DOMAIN" > "$MY127WS_TEST_OUTPUT" BASH ); - chmod($this->workspace()->path('fake-bin/docker'), 0755); - chmod($this->workspace()->path('fake-bin/docker-compose'), 0755); - $env['PATH'] = $fakeBin . ':' . getenv('PATH'); + $env['MY127WS_PROXY_DOMAIN'] = 'shell.example.test'; $env['MY127WS_TEST_OUTPUT'] = $this->workspace()->path('proxy-domain-output'); $this->workspace()->put('workspace.yml', <<<'YAML' @@ -93,35 +109,46 @@ public function testDirectMailServicePathExportsConfiguredProxyDomain(): void $this->workspaceCommand('direct service mail enable', null, $env); - self::assertSame("dev.example.test\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); + self::assertSame("shell.example.test\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); } public function testGlobalProxyServiceCommandPassesProjectProxyConfiguration(): void { - $env = $this->isolatedHomeEnvironment(); + $env = $this->isolatedHomeEnvironment(<<<'YAML' +attribute('global.service.proxy.domain'): global.example.test +attribute('global.service.proxy.https.crt'): https://certs.example.test/global.crt +attribute('global.service.proxy.https.key'): https://certs.example.test/global.key +YAML + ); $this->workspaceCommand('', null, $env); + $this->prepareFakeProxyServiceTools($env); $env['MY127WS_TEST_OUTPUT'] = $this->workspace()->path('proxy-service-output'); - $this->writeFakeWsServiceRecorder(); - $this->workspace()->put('workspace.yml', <<<'YAML' -attribute('global.service.proxy.domain'): project.example.test -attribute('global.service.proxy.https.crt'): https://certs.example.test/project.crt -attribute('global.service.proxy.https.key'): https://certs.example.test/project.key -attribute('global.service.proxy.https.crt_file'): project.crt -attribute('global.service.proxy.https.key_file'): project.key + $this->createProxyTestWorkspace(); + $this->workspace()->put('workspace.override.yml', <<<'YAML' +attribute.override('global.service.proxy.domain'): project.example.test +attribute.override('global.service.proxy.https.crt'): https://certs.example.test/project.crt +attribute.override('global.service.proxy.https.key'): https://certs.example.test/project.key +attribute.override('global.service.proxy.https.crt_file'): project.crt +attribute.override('global.service.proxy.https.key_file'): project.key YAML ); $this->workspaceCommand('global service proxy restart', null, $env); $expected = <<<'TEXT' -proxy restart project.example.test https://certs.example.test/project.crt https://certs.example.test/project.key project.crt project.key +tls: + stores: + default: + defaultCertificate: + certFile: /tls/project.crt + keyFile: /tls/project.key TEXT ; self::assertSame($expected . "\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); @@ -129,40 +156,97 @@ public function testGlobalProxyServiceCommandPassesProjectProxyConfiguration(): public function testGlobalProxyServiceCommandAllowsShellEnvironmentOverride(): void { - $env = $this->isolatedHomeEnvironment(); + $env = $this->isolatedHomeEnvironment(<<<'YAML' +attribute('global.service.proxy.domain'): global.example.test +attribute('global.service.proxy.https.crt'): https://certs.example.test/global.crt +attribute('global.service.proxy.https.key'): https://certs.example.test/global.key +YAML + ); $this->workspaceCommand('', null, $env); + $this->prepareFakeProxyServiceTools($env); $env['MY127WS_TEST_OUTPUT'] = $this->workspace()->path('proxy-service-output'); $env['MY127WS_PROXY_DOMAIN'] = 'shell.example.test'; $env['MY127WS_PROXY_HTTPS_CRT'] = 'https://certs.example.test/shell.crt'; $env['MY127WS_PROXY_HTTPS_KEY'] = 'https://certs.example.test/shell.key'; $env['MY127WS_PROXY_HTTPS_CRT_FILE'] = 'shell.crt'; $env['MY127WS_PROXY_HTTPS_KEY_FILE'] = 'shell.key'; - $this->writeFakeWsServiceRecorder(); - $this->workspace()->put('workspace.yml', <<<'YAML' -attribute('global.service.proxy.domain'): project.example.test -attribute('global.service.proxy.https.crt'): https://certs.example.test/project.crt -attribute('global.service.proxy.https.key'): https://certs.example.test/project.key -attribute('global.service.proxy.https.crt_file'): project.crt -attribute('global.service.proxy.https.key_file'): project.key + $this->createProxyTestWorkspace(); + $this->workspace()->put('workspace.override.yml', <<<'YAML' +attribute.override('global.service.proxy.domain'): project.example.test +attribute.override('global.service.proxy.https.crt'): https://certs.example.test/project.crt +attribute.override('global.service.proxy.https.key'): https://certs.example.test/project.key +attribute.override('global.service.proxy.https.crt_file'): project.crt +attribute.override('global.service.proxy.https.key_file'): project.key YAML ); $this->workspaceCommand('global service proxy restart', null, $env); $expected = <<<'TEXT' -proxy restart shell.example.test https://certs.example.test/shell.crt https://certs.example.test/shell.key shell.crt shell.key +tls: + stores: + default: + defaultCertificate: + certFile: /tls/shell.crt + keyFile: /tls/shell.key TEXT ; self::assertSame($expected . "\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); } + public function testGlobalProxyServiceCommandRejectsInvalidTlsFilename(): void + { + $env = $this->isolatedHomeEnvironment(); + $this->workspaceCommand('', null, $env); + $this->prepareFakeNoopProxyServiceTools($env); + + $this->createProxyTestWorkspace(); + $this->workspace()->put('workspace.override.yml', <<<'YAML' +attribute.override('global.service.proxy.domain'): project.example.test +attribute.override('global.service.proxy.https.crt'): https://certs.example.test/project.crt +attribute.override('global.service.proxy.https.key'): https://certs.example.test/project.key +attribute.override('global.service.proxy.https.crt_file'): ../project.crt +attribute.override('global.service.proxy.https.key_file'): project.key +YAML + ); + + $process = $this->workspaceProcess('global service proxy restart', null, $env); + $process->run(); + + self::assertNotSame(0, $process->getExitCode()); + self::assertStringContainsString('Invalid TLS filename: ../project.crt', $process->getErrorOutput()); + } + + public function testGlobalProxyServiceCommandRejectsSameTlsFilenames(): void + { + $env = $this->isolatedHomeEnvironment(); + $this->workspaceCommand('', null, $env); + $this->prepareFakeNoopProxyServiceTools($env); + + $this->createProxyTestWorkspace(); + $this->workspace()->put('workspace.override.yml', <<<'YAML' +attribute.override('global.service.proxy.domain'): project.example.test +attribute.override('global.service.proxy.https.crt'): https://certs.example.test/project.crt +attribute.override('global.service.proxy.https.key'): https://certs.example.test/project.key +attribute.override('global.service.proxy.https.crt_file'): project.pem +attribute.override('global.service.proxy.https.key_file'): project.pem +YAML + ); + + $process = $this->workspaceProcess('global service proxy restart', null, $env); + $process->run(); + + self::assertNotSame(0, $process->getExitCode()); + self::assertStringContainsString('TLS certificate and key filenames must be different.', $process->getErrorOutput()); + } + private function isolatedHomeEnvironment(?string $globalConfig = null): array { $home = $this->workspace()->path('home'); @@ -174,20 +258,74 @@ private function isolatedHomeEnvironment(?string $globalConfig = null): array return ['MY127WS_HOME' => $home]; } - private function writeFakeWsServiceRecorder(): void + private function createProxyTestWorkspace(): void { - $this->workspace()->put('home/.my127/workspace/bin/ws-service', <<<'BASH' + $this->workspace()->put('workspace.yml', "workspace('proxy-test'): ~\n"); + } + + private function prepareFakeProxyServiceTools(array &$env): void + { + $dockerComposeScript = <<<'BASH' #!/bin/bash { - printf '%s %s\n' "$1" "$2" printf '%s\n' "$MY127WS_PROXY_DOMAIN" printf '%s\n' "$MY127WS_PROXY_HTTPS_CRT" printf '%s\n' "$MY127WS_PROXY_HTTPS_KEY" printf '%s\n' "$MY127WS_PROXY_HTTPS_CRT_FILE" printf '%s\n' "$MY127WS_PROXY_HTTPS_KEY_FILE" + cat traefik/root/config/tls.yaml } > "$MY127WS_TEST_OUTPUT" +BASH; + + $this->prepareFakeServiceTools($env, $dockerComposeScript, true); + } + + private function prepareFakeNoopProxyServiceTools(array &$env): void + { + $dockerComposeScript = <<<'BASH' +#!/bin/bash +exit 0 +BASH; + + $this->prepareFakeServiceTools($env, $dockerComposeScript, true); + } + + private function prepareFakeServiceTools(array &$env, string $dockerComposeScript, bool $includeCurl = false): void + { + $root = dirname(__DIR__, 3); + $fakeBin = $this->workspace()->path('fake-bin'); + mkdir($fakeBin); + symlink($root . '/bin/workspace', $fakeBin . '/ws'); + $this->workspace()->put('fake-bin/docker', <<<'BASH' +#!/bin/bash +exit 0 BASH ); - chmod($this->workspace()->path('home/.my127/workspace/bin/ws-service'), 0755); + $this->workspace()->put('fake-bin/docker-compose', $dockerComposeScript); + chmod($this->workspace()->path('fake-bin/docker'), 0755); + chmod($this->workspace()->path('fake-bin/docker-compose'), 0755); + + if ($includeCurl) { + $this->workspace()->put('fake-bin/curl', <<<'BASH' +#!/bin/bash +output="" + +while [ "$#" -gt 0 ]; do + if [ "$1" = "--output" ]; then + shift + output="$1" + fi + shift || true +done + +if [ -n "$output" ]; then + printf 'fake certificate\n' > "$output" +fi +BASH + ); + chmod($this->workspace()->path('fake-bin/curl'), 0755); + } + + $env['PATH'] = $fakeBin . ':' . getenv('PATH'); } } From 3c7ac047bb1175701a035cf67cf43b7d63272540 Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Wed, 17 Jun 2026 20:19:01 +0200 Subject: [PATCH 16/19] feat: update documentation for proxy configuration with HTTPS requirements and project overrides --- docs/custom-proxy-domain.md | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/custom-proxy-domain.md b/docs/custom-proxy-domain.md index 606c0b11..183ee48e 100644 --- a/docs/custom-proxy-domain.md +++ b/docs/custom-proxy-domain.md @@ -143,9 +143,10 @@ attribute('global.service.proxy.https.crt'): https://raw.githubusercontent.com/m attribute('global.service.proxy.https.key'): https://raw.githubusercontent.com/my-org/workspace-proxy-certs/main/certs/dev.example.test/privkey.pem ``` -For organisation/private certificates, use an internal HTTP(S) location that is +For organisation/private certificates, use an internal HTTPS location that is reachable from developer machines, such as a private website available on the -company network or VPN. +company network or VPN. Plain HTTP should only be used for disposable test +material or when the local-network risk is explicitly accepted. The certificate must cover the configured domain and the subdomains used by projects and global services. For `dev.example.test`, the certificate should @@ -253,6 +254,10 @@ ws global service logger enable ws global service tracing restart ``` +If a project also overrides `domain`, remove or restore that project override, +run `ws harness prepare`, then restart or recreate the project containers using +the project's normal workflow. + ## Use the domain in a project Most Workspace harnesses define `domain: my127.site` in their harness @@ -270,6 +275,15 @@ Project hostnames should then be under that suffix, such as: my-project.dev.example.test ``` +After changing the project domain, regenerate the harness output: + +```bash +ws harness prepare +``` + +Then restart or recreate the project containers using the project's normal +workflow. + For custom harnesses or manually maintained Compose files, update whichever hostname configuration the project uses. Workspace only changes the Global Proxy domain and certificate; it does not rewrite project hostnames automatically. @@ -288,20 +302,25 @@ project configuration. Precedence: ```text -exported shell environment -project/global Workspace attributes +MY127WS_PROXY_* shell environment +project attribute.override(...) entries +global ~/.config/my127/workspace/proxy.yml attributes installed Workspace defaults ``` -For a project-specific proxy profile, set the proxy attributes in that project's -`workspace.override.yml`: +For a project/team-specific proxy profile, set the proxy attributes in the +project's committed `workspace.yml`. Use `attribute.override(...)` so the +project profile can override a machine-global `proxy.yml` when present: ```yaml -attribute('global.service.proxy.domain'): dev.example.test -attribute('global.service.proxy.https.crt'): https://proxy-config.example.internal/certs/dev.example.test/fullchain.pem -attribute('global.service.proxy.https.key'): https://proxy-config.example.internal/certs/dev.example.test/privkey.pem +attribute.override('global.service.proxy.domain'): dev.example.test +attribute.override('global.service.proxy.https.crt'): https://proxy-config.example.internal/certs/dev.example.test/fullchain.pem +attribute.override('global.service.proxy.https.key'): https://proxy-config.example.internal/certs/dev.example.test/privkey.pem ``` +Use `workspace.override.yml` only for developer-local proxy settings that should +not be committed. + Then restart the machine-global proxy from that project: ```bash From 283b292faa8e56d5c52b1e6c2fa36f09d4579d3b Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Thu, 18 Jun 2026 13:51:49 +0200 Subject: [PATCH 17/19] feat: implement proxy environment population and update service initialization to use global configuration --- bin/workspace | 1 + config/workspace/global.yml | 12 -------- docs/custom-proxy-domain.md | 21 ++++++++------ home/bin/ws-service | 27 +++++++++++++++++ home/service/logger/init.sh | 3 -- home/service/mail/init.sh | 3 -- home/service/proxy/init.sh | 12 ++++---- home/service/tracing/init.sh | 3 -- .../GlobalProxyConfigurationTest.php | 29 +++++++++++++++++++ 9 files changed, 75 insertions(+), 36 deletions(-) diff --git a/bin/workspace b/bin/workspace index 9c22906a..b59e9fe4 100755 --- a/bin/workspace +++ b/bin/workspace @@ -18,6 +18,7 @@ function main(): void install(); } + putenv('MY127WS_WORKSPACE_BIN='.($_SERVER['argv'][0] ?? __FILE__)); putenv('PATH='.home().'/.my127/workspace/bin:'.getenv('PATH')); $exitStatus = application()->run(); diff --git a/config/workspace/global.yml b/config/workspace/global.yml index 4132d77e..4e9149f3 100644 --- a/config/workspace/global.yml +++ b/config/workspace/global.yml @@ -1,36 +1,24 @@ command('global service logger (enable|disable)', 'global service logger'): description: Actions for the logging service - env: - MY127WS_PROXY_DOMAIN: "= env('MY127WS_PROXY_DOMAIN') ?: @('global.service.proxy.domain')" exec: | #!bash|= ws-service logger ={input.command(4)} command('global service mail (enable|disable)', 'global service mail'): description: Actions for mailhog - env: - MY127WS_PROXY_DOMAIN: "= env('MY127WS_PROXY_DOMAIN') ?: @('global.service.proxy.domain')" exec: | #!bash|= ws-service mail ={input.command(4)} command('global service proxy (enable|disable|restart)', 'global service proxy'): description: Actions for Traefik proxy - env: - MY127WS_PROXY_DOMAIN: "= env('MY127WS_PROXY_DOMAIN') ?: @('global.service.proxy.domain')" - MY127WS_PROXY_HTTPS_CRT: "= env('MY127WS_PROXY_HTTPS_CRT') ?: @('global.service.proxy.https.crt')" - MY127WS_PROXY_HTTPS_KEY: "= env('MY127WS_PROXY_HTTPS_KEY') ?: @('global.service.proxy.https.key')" - MY127WS_PROXY_HTTPS_CRT_FILE: "= env('MY127WS_PROXY_HTTPS_CRT_FILE') ?: @('global.service.proxy.https.crt_file')" - MY127WS_PROXY_HTTPS_KEY_FILE: "= env('MY127WS_PROXY_HTTPS_KEY_FILE') ?: @('global.service.proxy.https.key_file')" exec: | #!bash|= ws-service proxy ={input.command(4)} command('global service tracing (start|stop|restart)', 'global service tracing'): description: Actions for the tracing service - env: - MY127WS_PROXY_DOMAIN: "= env('MY127WS_PROXY_DOMAIN') ?: @('global.service.proxy.domain')" exec: | #!bash|= ws-service tracing ={input.command(4)} diff --git a/docs/custom-proxy-domain.md b/docs/custom-proxy-domain.md index 183ee48e..3abe00da 100644 --- a/docs/custom-proxy-domain.md +++ b/docs/custom-proxy-domain.md @@ -40,7 +40,8 @@ remote URL and it is not managed by a Workspace import command. flowchart LR configFile["~/.config/my127/workspace/proxy.yml"] attributes["global.service.proxy.* attributes"] - commands["ws global service proxy restart"] + commands["ws global service ...
ws install required services"] + serviceWrapper["home/bin/ws-service"] init["home/service/proxy/init.sh"] compose["docker-compose labels"] tlsFiles["traefik/root/tls/"] @@ -49,7 +50,8 @@ flowchart LR configFile --> attributes attributes --> commands - commands --> init + commands --> serviceWrapper + serviceWrapper --> init init --> compose init --> tlsFiles init --> tlsConfig @@ -58,9 +60,10 @@ flowchart LR tlsConfig --> traefik ``` -The proxy init script reads these attributes from global Workspace config, -downloads the configured certificate files, and exports the domain for Docker -Compose labels. The source of truth remains the global Workspace config file. +`ws-service` resolves proxy attributes before it calls a service `init.sh`. +For the proxy service it exports the domain and certificate settings; for the +other global services it exports the domain used by Docker Compose labels. The +source of truth remains Workspace config. ## Create the proxy config file @@ -294,10 +297,10 @@ different suffix. ## Switch the proxy per project -The Global Proxy still runs one domain at a time, but `ws global service ...` -commands resolve proxy settings before calling the service scripts. This allows -temporary overrides from either shell environment variables or the current -project configuration. +The Global Proxy still runs one domain at a time, but `ws-service` resolves +proxy settings before calling the service scripts. This keeps `ws global service +...`, required services started during `ws install`, and direct `ws-service ...` +calls on the same proxy configuration path. Precedence: diff --git a/home/bin/ws-service b/home/bin/ws-service index 51cae109..e44874d6 100755 --- a/home/bin/ws-service +++ b/home/bin/ws-service @@ -24,9 +24,36 @@ main() shift + populate_proxy_environment "$service" + "${DIR}/service/${service}/init.sh" "$@" } +populate_proxy_environment() +{ + local workspace_bin="${MY127WS_WORKSPACE_BIN:-}" + workspace_bin="${workspace_bin:-$(command -v ws 2> /dev/null || true)}" + + if [ -z "$workspace_bin" ]; then + echo "Warning: The ws command line tool is not available; proxy environment was not populated" >&2 + return + fi + + MY127WS_PROXY_DOMAIN="${MY127WS_PROXY_DOMAIN:-$("$workspace_bin" global config get global.service.proxy.domain)}" + export MY127WS_PROXY_DOMAIN + + if [ "$1" = "proxy" ]; then + MY127WS_PROXY_HTTPS_CRT="${MY127WS_PROXY_HTTPS_CRT:-$("$workspace_bin" global config get global.service.proxy.https.crt)}" + MY127WS_PROXY_HTTPS_KEY="${MY127WS_PROXY_HTTPS_KEY:-$("$workspace_bin" global config get global.service.proxy.https.key)}" + MY127WS_PROXY_HTTPS_CRT_FILE="${MY127WS_PROXY_HTTPS_CRT_FILE:-$("$workspace_bin" global config get global.service.proxy.https.crt_file)}" + MY127WS_PROXY_HTTPS_KEY_FILE="${MY127WS_PROXY_HTTPS_KEY_FILE:-$("$workspace_bin" global config get global.service.proxy.https.key_file)}" + export MY127WS_PROXY_HTTPS_CRT + export MY127WS_PROXY_HTTPS_KEY + export MY127WS_PROXY_HTTPS_CRT_FILE + export MY127WS_PROXY_HTTPS_KEY_FILE + fi +} + bootstrap() { DIR="$(cd "$(dirname "$0")" && cd ../ && pwd)" diff --git a/home/service/logger/init.sh b/home/service/logger/init.sh index 21d297f9..14ae1bf3 100755 --- a/home/service/logger/init.sh +++ b/home/service/logger/init.sh @@ -21,9 +21,6 @@ main() enable() { - MY127WS_PROXY_DOMAIN="${MY127WS_PROXY_DOMAIN:-$(ws global config get global.service.proxy.domain)}" - export MY127WS_PROXY_DOMAIN - run docker-compose -p my127ws-logger up -d --build touch .flag-built } diff --git a/home/service/mail/init.sh b/home/service/mail/init.sh index e134361a..fb0312c1 100755 --- a/home/service/mail/init.sh +++ b/home/service/mail/init.sh @@ -21,9 +21,6 @@ main() enable() { - MY127WS_PROXY_DOMAIN="${MY127WS_PROXY_DOMAIN:-$(ws global config get global.service.proxy.domain)}" - export MY127WS_PROXY_DOMAIN - run docker-compose -p my127ws-mail up -d --build touch .flag-built } diff --git a/home/service/proxy/init.sh b/home/service/proxy/init.sh index 74425eb9..8b9240a6 100755 --- a/home/service/proxy/init.sh +++ b/home/service/proxy/init.sh @@ -29,12 +29,12 @@ enable() cd "$DIR" if ! docker ps | grep my127ws-proxy > /dev/null; then - MY127WS_PROXY_DOMAIN="${MY127WS_PROXY_DOMAIN:-$(ws global config get global.service.proxy.domain)}" - MY127WS_PROXY_HTTPS_CRT="${MY127WS_PROXY_HTTPS_CRT:-$(ws global config get global.service.proxy.https.crt)}" - MY127WS_PROXY_HTTPS_KEY="${MY127WS_PROXY_HTTPS_KEY:-$(ws global config get global.service.proxy.https.key)}" - MY127WS_PROXY_HTTPS_CRT_FILE="${MY127WS_PROXY_HTTPS_CRT_FILE:-$(ws global config get global.service.proxy.https.crt_file)}" - MY127WS_PROXY_HTTPS_KEY_FILE="${MY127WS_PROXY_HTTPS_KEY_FILE:-$(ws global config get global.service.proxy.https.key_file)}" - export MY127WS_PROXY_DOMAIN + local PROXY_ENV_ERROR_MESSAGE="is required. Use ws global service proxy enable or ws global service proxy restart." + + : "${MY127WS_PROXY_HTTPS_CRT:?$PROXY_ENV_ERROR_MESSAGE}" + : "${MY127WS_PROXY_HTTPS_KEY:?$PROXY_ENV_ERROR_MESSAGE}" + : "${MY127WS_PROXY_HTTPS_CRT_FILE:?$PROXY_ENV_ERROR_MESSAGE}" + : "${MY127WS_PROXY_HTTPS_KEY_FILE:?$PROXY_ENV_ERROR_MESSAGE}" validate_tls_filenames "$MY127WS_PROXY_HTTPS_CRT_FILE" "$MY127WS_PROXY_HTTPS_KEY_FILE" diff --git a/home/service/tracing/init.sh b/home/service/tracing/init.sh index 727714d2..0b6ac962 100755 --- a/home/service/tracing/init.sh +++ b/home/service/tracing/init.sh @@ -28,9 +28,6 @@ start() ( cd "$DIR" - MY127WS_PROXY_DOMAIN="${MY127WS_PROXY_DOMAIN:-$(ws global config get global.service.proxy.domain)}" - export MY127WS_PROXY_DOMAIN - run docker-compose -p my127ws-tracing pull run docker-compose -p my127ws-tracing up -d diff --git a/tests/Test/Application/GlobalProxyConfigurationTest.php b/tests/Test/Application/GlobalProxyConfigurationTest.php index 9c0dc792..b212ae0d 100644 --- a/tests/Test/Application/GlobalProxyConfigurationTest.php +++ b/tests/Test/Application/GlobalProxyConfigurationTest.php @@ -112,6 +112,35 @@ public function testDirectMailServicePathPreservesShellProxyDomain(): void self::assertSame("shell.example.test\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); } + public function testDirectMailServicePathUsesProjectProxyConfiguration(): void + { + $env = $this->isolatedHomeEnvironment(<<<'YAML' +attribute('global.service.proxy.domain'): global.example.test +YAML + ); + $this->workspaceCommand('', null, $env); + + $this->prepareFakeServiceTools($env, <<<'BASH' +#!/bin/bash +echo "$MY127WS_PROXY_DOMAIN" > "$MY127WS_TEST_OUTPUT" +BASH + ); + $env['MY127WS_TEST_OUTPUT'] = $this->workspace()->path('proxy-domain-output'); + + $this->workspace()->put('workspace.yml', <<<'YAML' +workspace('proxy-test'): ~ +attribute.override('global.service.proxy.domain'): project.example.test +command('direct service mail enable'): | + #!bash + ws-service mail enable +YAML + ); + + $this->workspaceCommand('direct service mail enable', null, $env); + + self::assertSame("project.example.test\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); + } + public function testGlobalProxyServiceCommandPassesProjectProxyConfiguration(): void { $env = $this->isolatedHomeEnvironment(<<<'YAML' From adeb1b3147933f9000e887bbcd81943f215b5eec Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Thu, 18 Jun 2026 14:19:58 +0200 Subject: [PATCH 18/19] feat: update init script to use dynamic workspace binary reference for proxy restart --- docs/custom-proxy-domain.md | 18 ++++++++++-------- home/service/tracing/init.sh | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/custom-proxy-domain.md b/docs/custom-proxy-domain.md index 3abe00da..bec17192 100644 --- a/docs/custom-proxy-domain.md +++ b/docs/custom-proxy-domain.md @@ -60,10 +60,10 @@ flowchart LR tlsConfig --> traefik ``` -`ws-service` resolves proxy attributes before it calls a service `init.sh`. -For the proxy service it exports the domain and certificate settings; for the -other global services it exports the domain used by Docker Compose labels. The -source of truth remains Workspace config. +The internal `ws-service` wrapper resolves proxy attributes before it calls a +service `init.sh`. For the proxy service it exports the domain and certificate +settings; for the other global services it exports the domain used by Docker +Compose labels. The source of truth remains Workspace config. ## Create the proxy config file @@ -297,10 +297,10 @@ different suffix. ## Switch the proxy per project -The Global Proxy still runs one domain at a time, but `ws-service` resolves -proxy settings before calling the service scripts. This keeps `ws global service -...`, required services started during `ws install`, and direct `ws-service ...` -calls on the same proxy configuration path. +The Global Proxy still runs one domain at a time, but the internal service +wrapper resolves proxy settings before calling service scripts. This keeps +`ws global service ...` commands and required services started during +`ws install` on the same proxy configuration path. Precedence: @@ -338,6 +338,8 @@ running the restart: MY127WS_PROXY_DOMAIN=dev.example.test \ MY127WS_PROXY_HTTPS_CRT=https://proxy-config.example.internal/certs/dev.example.test/fullchain.pem \ MY127WS_PROXY_HTTPS_KEY=https://proxy-config.example.internal/certs/dev.example.test/privkey.pem \ +MY127WS_PROXY_HTTPS_CRT_FILE=dev.example.test.crt \ +MY127WS_PROXY_HTTPS_KEY_FILE=dev.example.test.key \ ws global service proxy restart ``` diff --git a/home/service/tracing/init.sh b/home/service/tracing/init.sh index 0b6ac962..59b713c4 100755 --- a/home/service/tracing/init.sh +++ b/home/service/tracing/init.sh @@ -36,7 +36,7 @@ start() cp "${TRAEFIK_CONFIG}" "${TRAEFIK_CONFIG}.before-tracing-active" sed 's/\[inactive.tracing\]/\[tracing\]/' "${TRAEFIK_CONFIG}.before-tracing-active" > "${TRAEFIK_CONFIG}" rm "${TRAEFIK_CONFIG}.before-tracing-active" - passthru ws global service proxy restart + passthru "${MY127WS_WORKSPACE_BIN:-ws}" global service proxy restart fi ) @@ -51,7 +51,7 @@ stop() sed 's/\[tracing\]/\[inactive.tracing\]/' "${TRAEFIK_CONFIG}.before-tracing-inactive" > "${TRAEFIK_CONFIG}" rm "${TRAEFIK_CONFIG}.before-tracing-inactive" if [ "$DO_PROXY_RESTART" = "yes" ]; then - passthru ws global service proxy restart + passthru "${MY127WS_WORKSPACE_BIN:-ws}" global service proxy restart fi fi run docker-compose -p my127ws-tracing down -v --rmi local From 1b9e48b0b42b4d32be7a5d6d12cad7104eaa7019 Mon Sep 17 00:00:00 2001 From: MarcoMC Date: Thu, 18 Jun 2026 15:39:12 +0200 Subject: [PATCH 19/19] feat: enhance workspace binary reference handling and update proxy environment population --- bin/workspace | 4 +- docs/custom-proxy-domain.md | 70 ++-------- home/bin/ws-service | 19 +-- .../GlobalProxyConfigurationTest.php | 128 ++++++------------ 4 files changed, 58 insertions(+), 163 deletions(-) diff --git a/bin/workspace b/bin/workspace index b59e9fe4..ee13d415 100755 --- a/bin/workspace +++ b/bin/workspace @@ -18,7 +18,9 @@ function main(): void install(); } - putenv('MY127WS_WORKSPACE_BIN='.($_SERVER['argv'][0] ?? __FILE__)); + $workspaceBin = $_SERVER['argv'][0] ?? __FILE__; + $workspaceBin = realpath($workspaceBin) ?: $workspaceBin; + putenv('MY127WS_WORKSPACE_BIN='.$workspaceBin); putenv('PATH='.home().'/.my127/workspace/bin:'.getenv('PATH')); $exitStatus = application()->run(); diff --git a/docs/custom-proxy-domain.md b/docs/custom-proxy-domain.md index bec17192..a2d87cea 100644 --- a/docs/custom-proxy-domain.md +++ b/docs/custom-proxy-domain.md @@ -16,7 +16,6 @@ domains. - [Apply the change](#apply-the-change) - [Revert to the default domain](#revert-to-the-default-domain) - [Use the domain in a project](#use-the-domain-in-a-project) -- [Switch the proxy per project](#switch-the-proxy-per-project) - [Renew certificates](#renew-certificates) ## How it fits together @@ -60,10 +59,13 @@ flowchart LR tlsConfig --> traefik ``` -The internal `ws-service` wrapper resolves proxy attributes before it calls a -service `init.sh`. For the proxy service it exports the domain and certificate -settings; for the other global services it exports the domain used by Docker -Compose labels. The source of truth remains Workspace config. +The internal `ws-service` wrapper resolves proxy attributes from the installed +global Workspace config before it calls a service `init.sh`. For the proxy +service it exports the domain and certificate settings; for the other global +services it exports the domain used by Docker Compose labels. + +Project `workspace.yml` files do not override the Global Proxy runtime. Use +`~/.config/my127/workspace/proxy.yml` to switch the machine-global proxy. ## Create the proxy config file @@ -291,61 +293,9 @@ For custom harnesses or manually maintained Compose files, update whichever hostname configuration the project uses. Workspace only changes the Global Proxy domain and certificate; it does not rewrite project hostnames automatically. -This simplified proxy configuration supports one certificate/domain set at a -time, so switch the global proxy before working on a project that uses a -different suffix. - -## Switch the proxy per project - -The Global Proxy still runs one domain at a time, but the internal service -wrapper resolves proxy settings before calling service scripts. This keeps -`ws global service ...` commands and required services started during -`ws install` on the same proxy configuration path. - -Precedence: - -```text -MY127WS_PROXY_* shell environment -project attribute.override(...) entries -global ~/.config/my127/workspace/proxy.yml attributes -installed Workspace defaults -``` - -For a project/team-specific proxy profile, set the proxy attributes in the -project's committed `workspace.yml`. Use `attribute.override(...)` so the -project profile can override a machine-global `proxy.yml` when present: - -```yaml -attribute.override('global.service.proxy.domain'): dev.example.test -attribute.override('global.service.proxy.https.crt'): https://proxy-config.example.internal/certs/dev.example.test/fullchain.pem -attribute.override('global.service.proxy.https.key'): https://proxy-config.example.internal/certs/dev.example.test/privkey.pem -``` - -Use `workspace.override.yml` only for developer-local proxy settings that should -not be committed. - -Then restart the machine-global proxy from that project: - -```bash -cd path/to/project -ws global service proxy restart -``` - -For a one-off shell override, export or inline all matching proxy values before -running the restart: - -```bash -MY127WS_PROXY_DOMAIN=dev.example.test \ -MY127WS_PROXY_HTTPS_CRT=https://proxy-config.example.internal/certs/dev.example.test/fullchain.pem \ -MY127WS_PROXY_HTTPS_KEY=https://proxy-config.example.internal/certs/dev.example.test/privkey.pem \ -MY127WS_PROXY_HTTPS_CRT_FILE=dev.example.test.crt \ -MY127WS_PROXY_HTTPS_KEY_FILE=dev.example.test.key \ -ws global service proxy restart -``` - -Inline environment values are useful for temporary testing. Project attributes -are easier to repeat when regularly switching between company and personal -proxy domains. +This proxy configuration supports one certificate/domain set at a time, so +change `proxy.yml` and restart the global services before working on a project +that uses a different suffix. ## Renew certificates diff --git a/home/bin/ws-service b/home/bin/ws-service index e44874d6..5cdb8221 100755 --- a/home/bin/ws-service +++ b/home/bin/ws-service @@ -24,6 +24,7 @@ main() shift + cd "$DIR" populate_proxy_environment "$service" "${DIR}/service/${service}/init.sh" "$@" @@ -31,22 +32,16 @@ main() populate_proxy_environment() { - local workspace_bin="${MY127WS_WORKSPACE_BIN:-}" - workspace_bin="${workspace_bin:-$(command -v ws 2> /dev/null || true)}" + local workspace_bin="${MY127WS_WORKSPACE_BIN:-ws}" - if [ -z "$workspace_bin" ]; then - echo "Warning: The ws command line tool is not available; proxy environment was not populated" >&2 - return - fi - - MY127WS_PROXY_DOMAIN="${MY127WS_PROXY_DOMAIN:-$("$workspace_bin" global config get global.service.proxy.domain)}" + MY127WS_PROXY_DOMAIN="$("$workspace_bin" global config get global.service.proxy.domain)" export MY127WS_PROXY_DOMAIN if [ "$1" = "proxy" ]; then - MY127WS_PROXY_HTTPS_CRT="${MY127WS_PROXY_HTTPS_CRT:-$("$workspace_bin" global config get global.service.proxy.https.crt)}" - MY127WS_PROXY_HTTPS_KEY="${MY127WS_PROXY_HTTPS_KEY:-$("$workspace_bin" global config get global.service.proxy.https.key)}" - MY127WS_PROXY_HTTPS_CRT_FILE="${MY127WS_PROXY_HTTPS_CRT_FILE:-$("$workspace_bin" global config get global.service.proxy.https.crt_file)}" - MY127WS_PROXY_HTTPS_KEY_FILE="${MY127WS_PROXY_HTTPS_KEY_FILE:-$("$workspace_bin" global config get global.service.proxy.https.key_file)}" + MY127WS_PROXY_HTTPS_CRT="$("$workspace_bin" global config get global.service.proxy.https.crt)" + MY127WS_PROXY_HTTPS_KEY="$("$workspace_bin" global config get global.service.proxy.https.key)" + MY127WS_PROXY_HTTPS_CRT_FILE="$("$workspace_bin" global config get global.service.proxy.https.crt_file)" + MY127WS_PROXY_HTTPS_KEY_FILE="$("$workspace_bin" global config get global.service.proxy.https.key_file)" export MY127WS_PROXY_HTTPS_CRT export MY127WS_PROXY_HTTPS_KEY export MY127WS_PROXY_HTTPS_CRT_FILE diff --git a/tests/Test/Application/GlobalProxyConfigurationTest.php b/tests/Test/Application/GlobalProxyConfigurationTest.php index b212ae0d..521af182 100644 --- a/tests/Test/Application/GlobalProxyConfigurationTest.php +++ b/tests/Test/Application/GlobalProxyConfigurationTest.php @@ -84,35 +84,7 @@ public function testDirectMailServicePathExportsConfiguredProxyDomain(): void self::assertSame("dev.example.test\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); } - public function testDirectMailServicePathPreservesShellProxyDomain(): void - { - $env = $this->isolatedHomeEnvironment(<<<'YAML' -attribute('global.service.proxy.domain'): dev.example.test -YAML - ); - $this->workspaceCommand('', null, $env); - - $this->prepareFakeServiceTools($env, <<<'BASH' -#!/bin/bash -echo "$MY127WS_PROXY_DOMAIN" > "$MY127WS_TEST_OUTPUT" -BASH - ); - $env['MY127WS_PROXY_DOMAIN'] = 'shell.example.test'; - $env['MY127WS_TEST_OUTPUT'] = $this->workspace()->path('proxy-domain-output'); - - $this->workspace()->put('workspace.yml', <<<'YAML' -command('direct service mail enable'): | - #!bash - ws-service mail enable -YAML - ); - - $this->workspaceCommand('direct service mail enable', null, $env); - - self::assertSame("shell.example.test\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); - } - - public function testDirectMailServicePathUsesProjectProxyConfiguration(): void + public function testDirectMailServicePathIgnoresProjectProxyConfiguration(): void { $env = $this->isolatedHomeEnvironment(<<<'YAML' attribute('global.service.proxy.domain'): global.example.test @@ -138,10 +110,10 @@ public function testDirectMailServicePathUsesProjectProxyConfiguration(): void $this->workspaceCommand('direct service mail enable', null, $env); - self::assertSame("project.example.test\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); + self::assertSame("global.example.test\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); } - public function testGlobalProxyServiceCommandPassesProjectProxyConfiguration(): void + public function testGlobalProxyServiceCommandUsesGlobalConfigurationFromProject(): void { $env = $this->isolatedHomeEnvironment(<<<'YAML' attribute('global.service.proxy.domain'): global.example.test @@ -154,77 +126,57 @@ public function testGlobalProxyServiceCommandPassesProjectProxyConfiguration(): $this->prepareFakeProxyServiceTools($env); $env['MY127WS_TEST_OUTPUT'] = $this->workspace()->path('proxy-service-output'); - $this->createProxyTestWorkspace(); - $this->workspace()->put('workspace.override.yml', <<<'YAML' + $this->workspace()->put('workspace.yml', <<<'YAML' +workspace('proxy-test'): ~ attribute.override('global.service.proxy.domain'): project.example.test attribute.override('global.service.proxy.https.crt'): https://certs.example.test/project.crt attribute.override('global.service.proxy.https.key'): https://certs.example.test/project.key -attribute.override('global.service.proxy.https.crt_file'): project.crt -attribute.override('global.service.proxy.https.key_file'): project.key YAML ); $this->workspaceCommand('global service proxy restart', null, $env); $expected = <<<'TEXT' -project.example.test -https://certs.example.test/project.crt -https://certs.example.test/project.key -project.crt -project.key +global.example.test +https://certs.example.test/global.crt +https://certs.example.test/global.key +global.example.test.crt +global.example.test.key tls: stores: default: defaultCertificate: - certFile: /tls/project.crt - keyFile: /tls/project.key + certFile: /tls/global.example.test.crt + keyFile: /tls/global.example.test.key TEXT ; self::assertSame($expected . "\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); } - public function testGlobalProxyServiceCommandAllowsShellEnvironmentOverride(): void + public function testGlobalProxyServiceCommandUsesInstalledDefaultsFromProject(): void { - $env = $this->isolatedHomeEnvironment(<<<'YAML' -attribute('global.service.proxy.domain'): global.example.test -attribute('global.service.proxy.https.crt'): https://certs.example.test/global.crt -attribute('global.service.proxy.https.key'): https://certs.example.test/global.key -YAML - ); + $env = $this->isolatedHomeEnvironment(); $this->workspaceCommand('', null, $env); $this->prepareFakeProxyServiceTools($env); $env['MY127WS_TEST_OUTPUT'] = $this->workspace()->path('proxy-service-output'); - $env['MY127WS_PROXY_DOMAIN'] = 'shell.example.test'; - $env['MY127WS_PROXY_HTTPS_CRT'] = 'https://certs.example.test/shell.crt'; - $env['MY127WS_PROXY_HTTPS_KEY'] = 'https://certs.example.test/shell.key'; - $env['MY127WS_PROXY_HTTPS_CRT_FILE'] = 'shell.crt'; - $env['MY127WS_PROXY_HTTPS_KEY_FILE'] = 'shell.key'; $this->createProxyTestWorkspace(); - $this->workspace()->put('workspace.override.yml', <<<'YAML' -attribute.override('global.service.proxy.domain'): project.example.test -attribute.override('global.service.proxy.https.crt'): https://certs.example.test/project.crt -attribute.override('global.service.proxy.https.key'): https://certs.example.test/project.key -attribute.override('global.service.proxy.https.crt_file'): project.crt -attribute.override('global.service.proxy.https.key_file'): project.key -YAML - ); $this->workspaceCommand('global service proxy restart', null, $env); $expected = <<<'TEXT' -shell.example.test -https://certs.example.test/shell.crt -https://certs.example.test/shell.key -shell.crt -shell.key +my127.site +https://my127.io/workspace/my127.site.crt +https://my127.io/workspace/my127.site.key +my127.site.crt +my127.site.key tls: stores: default: defaultCertificate: - certFile: /tls/shell.crt - keyFile: /tls/shell.key + certFile: /tls/my127.site.crt + keyFile: /tls/my127.site.key TEXT ; self::assertSame($expected . "\n", file_get_contents($env['MY127WS_TEST_OUTPUT'])); @@ -232,43 +184,39 @@ public function testGlobalProxyServiceCommandAllowsShellEnvironmentOverride(): v public function testGlobalProxyServiceCommandRejectsInvalidTlsFilename(): void { - $env = $this->isolatedHomeEnvironment(); + $env = $this->isolatedHomeEnvironment(<<<'YAML' +attribute('global.service.proxy.domain'): global.example.test +attribute('global.service.proxy.https.crt'): https://certs.example.test/global.crt +attribute('global.service.proxy.https.key'): https://certs.example.test/global.key +attribute('global.service.proxy.https.crt_file'): ../global.crt +attribute('global.service.proxy.https.key_file'): global.key +YAML + ); $this->workspaceCommand('', null, $env); $this->prepareFakeNoopProxyServiceTools($env); $this->createProxyTestWorkspace(); - $this->workspace()->put('workspace.override.yml', <<<'YAML' -attribute.override('global.service.proxy.domain'): project.example.test -attribute.override('global.service.proxy.https.crt'): https://certs.example.test/project.crt -attribute.override('global.service.proxy.https.key'): https://certs.example.test/project.key -attribute.override('global.service.proxy.https.crt_file'): ../project.crt -attribute.override('global.service.proxy.https.key_file'): project.key -YAML - ); - $process = $this->workspaceProcess('global service proxy restart', null, $env); $process->run(); self::assertNotSame(0, $process->getExitCode()); - self::assertStringContainsString('Invalid TLS filename: ../project.crt', $process->getErrorOutput()); + self::assertStringContainsString('Invalid TLS filename: ../global.crt', $process->getErrorOutput()); } public function testGlobalProxyServiceCommandRejectsSameTlsFilenames(): void { - $env = $this->isolatedHomeEnvironment(); + $env = $this->isolatedHomeEnvironment(<<<'YAML' +attribute('global.service.proxy.domain'): global.example.test +attribute('global.service.proxy.https.crt'): https://certs.example.test/global.crt +attribute('global.service.proxy.https.key'): https://certs.example.test/global.key +attribute('global.service.proxy.https.crt_file'): global.pem +attribute('global.service.proxy.https.key_file'): global.pem +YAML + ); $this->workspaceCommand('', null, $env); $this->prepareFakeNoopProxyServiceTools($env); $this->createProxyTestWorkspace(); - $this->workspace()->put('workspace.override.yml', <<<'YAML' -attribute.override('global.service.proxy.domain'): project.example.test -attribute.override('global.service.proxy.https.crt'): https://certs.example.test/project.crt -attribute.override('global.service.proxy.https.key'): https://certs.example.test/project.key -attribute.override('global.service.proxy.https.crt_file'): project.pem -attribute.override('global.service.proxy.https.key_file'): project.pem -YAML - ); - $process = $this->workspaceProcess('global service proxy restart', null, $env); $process->run();