diff --git a/.github/workflows/go_generate_update.yml b/.github/workflows/go_generate_update.yml index 15f1567e..6f364ee2 100644 --- a/.github/workflows/go_generate_update.yml +++ b/.github/workflows/go_generate_update.yml @@ -5,6 +5,12 @@ on: schedule: - cron: '0 8-18/4 * * 1-5' +env: + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_NAME: github-actions[bot] + GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + jobs: update: name: Update generated code @@ -15,36 +21,51 @@ jobs: uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.18 - - uses: actions/cache@v3 + uses: actions/setup-go@v5 with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + go-version-file: 'go.mod' - - name: Prepare - run: go generate ./ + name: Prepare, generate, and format code + run: | + rm -rf ~/.platformsh/bin/ + go generate ./... - name: Check Git status id: git run: | - RESULT=$(git status --untracked-files=no --porcelain) - echo "::set-output name=gitstatus::$RESULT" + { + echo 'gitstatus<> $GITHUB_OUTPUT - name: Test if: steps.git.outputs.gitstatus != '' run: go test -v ./... + - name: Validate build + if: steps.git.outputs.gitstatus != '' + run: go run . + - - name: Commit and push the update + name: Commit the update for PSH-related code if: steps.git.outputs.gitstatus != '' run: | - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config user.name "github-actions[bot]" git add local/platformsh/commands.go local/platformsh/config.go - git commit -m "chore: Update supported Platform.sh services" + git diff --staged --quiet || git commit -m "Update supported Platform.sh services" + + - name: Commit the update for Docker-related code + if: steps.git.outputs.gitstatus != '' + run: | + git add envs/docker_version.go + git diff --staged --quiet || git commit -m "Update Docker Client version" + + - name: Commit the update for PHP-related code + if: steps.git.outputs.gitstatus != '' + run: | + git add commands/php_version.go + git diff --staged --quiet || git commit -m "Update latest available PHP version" + + - name: Commit and push the updates + if: steps.git.outputs.gitstatus != '' + run: | git push diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index 4f44ae49..9c3d11a2 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -7,43 +7,48 @@ on: permissions: contents: write id-token: write + packages: write jobs: releaser: name: Release runs-on: ubuntu-latest + env: + # We need to set DOCKER_CLI_EXPERIMENTAL=enabled for the docker manifest commands to work + DOCKER_CLI_EXPERIMENTAL: "enabled" steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 + + # We need QEMU to use buildx and be able to build ARM Docker images + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Login into Github Docker Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + if: startsWith(github.ref, 'refs/tags/v') + - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.18 - - uses: actions/cache@v3 + uses: actions/setup-go@v5 with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + go-version-file: 'go.mod' - name: Set AUTOUPDATE_CHANNEL on tags run: echo "AUTOUPDATE_CHANNEL=stable" >> $GITHUB_ENV if: startsWith(github.ref, 'refs/tags/v') - name: Prepare - run: go generate ./ + run: go generate ./... - name: Check Git status id: git run: | RESULT=$(git status --untracked-files=no --porcelain) - echo "::set-output name=gitstatus::$RESULT" + echo "gitstatus=$RESULT" >> $GITHUB_OUTPUT - name: Check if go prepare updated generated Go code if: steps.git.outputs.gitstatus != '' && startsWith(github.ref, 'refs/tags/v') @@ -54,51 +59,46 @@ jobs: - name: Test run: go test -v ./... + - name: Validate build + run: go run . - name: Set up cosign - uses: sigstore/cosign-installer@v2.0.0 + uses: sigstore/cosign-installer@v3 - name: Run GoReleaser for snapshot - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v6 # only for PRs and push on branches - if: startsWith(github.ref, 'refs/heads/') + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} with: - version: latest - args: build --rm-dist --snapshot + version: '~> v2' + args: release --clean --snapshot --skip=publish,sign env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v6 # only for tags if: startsWith(github.ref, 'refs/tags/v') with: - version: latest - args: release --rm-dist + version: '~> v2' + args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAP_GITHUB_TOKEN: ${{ secrets.GH_PAT }} - name: Archive binaries - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: retention-days: 5 + name: binaries path: dist - - name: Fury Uploads - env: - FURY_PUSH_TOKEN: ${{ secrets.FURY_PUSH_TOKEN }} - if: startsWith(github.ref, 'refs/tags/v') - run: | - URLS=`curl -fsSL "https://api.github.com/repos/symfony-cli/symfony-cli/releases/latest" | jq -r '.assets[] | select(.name | match("(deb|rpm)$")).browser_download_url'` - for URL in $URLS - do - readarray -d "/" -t arr <<< "$URL" - NAME=${arr[-1]} - curl -fsSL $URL > /tmp/$NAME - curl https://$FURY_PUSH_TOKEN@push.fury.io/symfony/ -F package=@/tmp/$NAME - unlink /tmp/$NAME - done + name: Archive Linux binary + uses: actions/upload-artifact@v4 + with: + retention-days: 5 + name: linux-binary + path: dist/symfony-cli_linux_amd64.tar.gz - name: Install Cloudsmith CLI run: pip install --upgrade cloudsmith-cli @@ -115,5 +115,5 @@ jobs: cloudsmith push rpm symfony/stable/any-distro/any-version $filename done for filename in dist/*.apk; do - cloudsmith push alpine symfony/stable/any-distro/any-version $filename + cloudsmith push alpine symfony/stable/alpine/any-version $filename done diff --git a/.gitignore b/.gitignore index c9b8625e..f485b1cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /dist /vendor +symfony-cli diff --git a/.goreleaser.yml b/.goreleaser.yml index 45133383..82ab262c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,5 @@ +version: 2 + before: hooks: - go mod download @@ -20,12 +22,23 @@ builds: goarch: - 386 - amd64 + - arm - arm64 + goarm: + # Because of a limitation in DEB packaging we can only build and package + # a single ARMv6 or v7 variant at a single time. As ARMv6 is upwards + # compatible with ARMv7 so let's only build ARMv6 here (default value + # anyway) + - 6 ignore: + - goos: windows + goarch: arm - goos: windows goarch: arm64 - goos: darwin goarch: 386 + - goos: darwin + goarch: arm main: ./ binary: symfony ldflags: -s -w -X 'main.channel={{ if index .Env "AUTOUPDATE_CHANNEL" }}{{ .Env.AUTOUPDATE_CHANNEL }}{{ else }}dev{{ end }}' -X 'main.buildDate={{ .Date }}' -X 'main.version={{ .Version }}' @@ -50,7 +63,7 @@ source: enabled: true snapshot: - name_template: "next" + version_template: "next" universal_binaries: - replace: true @@ -60,14 +73,13 @@ universal_binaries: # https://goreleaser.com/customization/sign signs: - cmd: cosign - env: - - COSIGN_EXPERIMENTAL=1 certificate: '${artifact}.pem' args: - sign-blob - '--output-certificate=${certificate}' - '--output-signature=${signature}' - '${artifact}' + - "--yes" artifacts: all output: true @@ -76,17 +88,24 @@ release: **Full Changelog**: https://github.com/symfony-cli/symfony-cli/compare/{{ .PreviousTag }}...{{ .Tag }} brews: - - tap: + - repository: owner: symfony-cli name: homebrew-tap token: "{{ .Env.TAP_GITHUB_TOKEN }}" commit_author: name: Fabien Potencier email: fabien@symfony.com - folder: Formula - goarm: "7" + directory: Formula + # Homebrew supports only a single GOARM variant and ARMv6 is upwards + # compatible with ARMv7 so let's keep ARMv6 here (default value anyway) + goarm: "6" homepage: https://symfony.com description: Symfony CLI helps Symfony developers manage projects, from local code to remote infrastructure + caveats: |- + To install shell completions, add this to your profile: + if command -v symfony &>/dev/null; then + eval "$(symfony completion)" + fi license: AGPL-3.0 test: | system "#{bin}/symfony version" @@ -95,6 +114,9 @@ brews: type: optional install: |- bin.install "symfony" + service: |- + run ["#{bin}/symfony", "local:proxy:start", "--foreground"] + keep_alive true nfpms: - file_name_template: '{{ .ConventionalFileName }}' @@ -110,3 +132,58 @@ nfpms: - rpm recommends: - git + +dockers: + - image_templates: [ "ghcr.io/symfony-cli/{{ .ProjectName }}:{{ .Version }}-amd64" ] + goarch: amd64 + use: buildx + build_flag_templates: + - "--pull" + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.source=https://github.com/symfony-cli/symfony-cli" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - image_templates: [ "ghcr.io/symfony-cli/{{ .ProjectName }}:{{ .Version }}-arm64" ] + goarch: arm64 + use: buildx + build_flag_templates: + - "--pull" + - "--platform=linux/arm64" + - "--label=org.opencontainers.image.source=https://github.com/symfony-cli/symfony-cli" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - image_templates: [ "ghcr.io/symfony-cli/{{ .ProjectName }}:{{ .Version }}-arm32v6" ] + goarch: arm + goarm: '6' + use: buildx + build_flag_templates: + - "--pull" + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.source=https://github.com/symfony-cli/symfony-cli" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - image_templates: [ "ghcr.io/symfony-cli/{{ .ProjectName }}:{{ .Version }}-arm32v7" ] + goarch: arm + # ARMv6 is upwards compatible with ARMv7 + goarm: '6' + use: buildx + build_flag_templates: + - "--pull" + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.source=https://github.com/symfony-cli/symfony-cli" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.version={{.Version}}" + +docker_manifests: + - name_template: ghcr.io/symfony-cli/{{ .ProjectName }}:{{ .Version }} + image_templates: &docker_images + - ghcr.io/symfony-cli/{{ .ProjectName }}:{{ .Version }}-amd64 + - ghcr.io/symfony-cli/{{ .ProjectName }}:{{ .Version }}-arm64 + - ghcr.io/symfony-cli/{{ .ProjectName }}:{{ .Version }}-arm32v6 + - ghcr.io/symfony-cli/{{ .ProjectName }}:{{ .Version }}-arm32v7 + - name_template: ghcr.io/symfony-cli/{{ .ProjectName }}:v{{ .Major }} + image_templates: *docker_images + - name_template: ghcr.io/symfony-cli/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }} + image_templates: *docker_images + - name_template: ghcr.io/symfony-cli/{{ .ProjectName }}:latest + image_templates: *docker_images diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f8ec4a7d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,153 @@ +# Contributing + +This guide is meant to help you start contributing to the Symfony CLI by +providing some key hints and explaining specifics related to this project. + +## Language choice + +First-time contributors could be surprised by the fact that this project is +written in Go whereas it is highly related to the Symfony Framework which is +written in PHP. + +Go has been picked because it is well suited for system development and has +close-to-zero runtime dependencies which make releasing quite easy. This is +ideal for a tool that is used on a wide range of platforms and potentially on +systems where the requirements to run Symfony are not met. + +Another reason is to make Symfony CLI independent of the PHP version because +one goal of the CLI is to make it possible to use many different versions of +PHP. + +Finally, Go is also usually quite easy to apprehend for PHP developers having +some similarities in their approach. + +## Setup Go + +Contributing to the CLI, implies that one must first setup Go locally on their +machine. Instructions are available on the official +[Go website](https://golang.org/dl). Just pick the latest version available: Go +will automatically download the version currently in use in the project and +dependencies as required. + +## Local setup + +First fork this repository and clone it to some location of your liking. Next, +try to build and run the project: + +```bash +$ go build . +``` + +If any error happen you must fix them before going on. If no error happen, this +should produce a binary in the project directory. By default, this binary is +named `symfony-cli` and suffixed with `.exe` on Windows. + +You should be able to run it right away: + +```bash +$ ./symfony-cli version +``` + +The binary is self-contained: you can copy it as-is to another system and/or +execute it without any installation process. + +> *Tip:* This binary can be executed from anywhere by using it's absolute path. +> This is handy during development when you need to run it in a project +> directory and you don't want to overwrite your system-wide Symfony CLI. + +Finally, before and after changing code you should ensure tests are passing: + +```bash +$ go test ./... +``` + +## Coding style + +The CLI follows the Go standard code formatting. To fix the code formatting one +can use the following command: + +```bash +$ go fmt ./... +``` + +One can also uses the `go vet` command in order to fix common mistakes: + +```bash +$ go vet ./... +``` + +## Cross compilation + +By definition, the CLI has to support multiple platforms which means that at +some point you might need to compile the code for another platform than the one +your are using to develop. + +This can be done using Go cross-platform compiling capabilities. For example +the following command will compile the CLI for Windows: + +```bash +$ GOOS=windows go build . +``` + +`GOOS` and `GOARCH` environment variables are used to target another OS or CPU +architecture, respectively. + +During development, please take into consideration (in particular in the +process and file management sections) that we currently support the following +platforms matrix: + +- Linux / 386 +- Linux / amd64 +- Linux / arm +- Linux / arm64 +- Darwin / amd64 +- Darwin / arm64 +- Windows / 386 +- Windows / amd64 + +## Code generation + +Part of the code is generated automatically. One should not need to regenerate +the code themselves because a GitHub Action is in-charge of it. In the +eventuality one would need to debug it, code generation can be run as follows: + +```bash +$ go generate ./... +``` + +If you add a new code generation command, please also update the GitHub +workflow in `.github/workflows/go_generate_update.yml`. + +## Additional repositories + +Contrary to the Symfony PHP Framework which is a mono-repository, the CLI +tool is developed in multiple repositories. `symfony-cli/symfony-cli` is the +main repository where lies most of the logic and is the only repository +producing a binary. + +Every other repository is mostly independent and it is highly unlikely that +you would need to have a look at them. However, in the eventuality where you +would have to, here is the description of each repository scope: +- `symfony-cli/phpstore` is an independent library in charge of the PHP + installations discovery and the logic to match a specific version to a given + version constraint. +- `symfony-cli/console` is an independent library created to ease the process + of Go command-line application. This library has been created with the goal + of mimicking the look and feel of the Symfony Console for the end-user. +- `symfony-cli/terminal` is a wrapper around the Input and Output in a command + line context. It provides helpers around styling (output formatters and + styling - à la Symfony) and interactivity (spinners and questions helpers) +- `symfony-cli/dumper` is a library similar to Symfony's VarDumper component + providing a `Dump` function useful to introspect variables content and + particularly useful in the strictly typed context of Go. + +If you ever have to work on those package, you can setup your local working +copy of the CLI to work with a local copy of one of those package by using +`go work`: + +```bash +$ go work init +$ go work use . +$ go work use /path/to/package/fork +# repeat last command for each package you want to work with +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..1ae27216 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM scratch as build + +COPY --from=composer:latest /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +COPY symfony /usr/local/bin/ + +FROM scratch + +ENV SYMFONY_ALLOW_ALL_IP=true + +ENTRYPOINT ["/usr/local/bin/symfony"] + +COPY --from=build . . diff --git a/README.md b/README.md index 0039c10c..a84d6fad 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,16 @@ Security Issues If you discover a security vulnerability, please follow our [disclosure procedure][11]. +Sponsorship [](https://cloudsmith.io/) +----------- + +Package repository hosting is graciously provided by +[cloudsmith](https://cloudsmith.io/). Cloudsmith is the only fully hosted, +cloud-native, universal package management solution, that enables your +organization to create, store and share packages in any format, to any place, +with total confidence. We believe there’s a better way to manage software +assets and packages, and they're making it happen! + [1]: https://symfony.com/download [2]: https://symfony.com/doc/current/setup.html#creating-symfony-applications [3]: https://symfony.com/doc/current/setup/symfony_server.html diff --git a/book/checkout.go b/book/checkout.go index e0676bac..33c6b507 100644 --- a/book/checkout.go +++ b/book/checkout.go @@ -29,6 +29,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/symfony-cli/symfony-cli/local/platformsh" "github.com/symfony-cli/terminal" ) @@ -64,8 +65,6 @@ func (b *Book) Checkout(step string) error { terminal.Println("[ OK ]") } - // FIXME: SQL dump? - if !b.Force && !b.AutoConfirm && !terminal.AskConfirmation("WARNING All current code, data, and containers are going to be REMOVED, do you confirm?", true) { return nil } @@ -101,15 +100,16 @@ func (b *Book) Checkout(step string) error { terminal.Println("[ OK ]") } - _, err = os.Stat(filepath.Join(b.Dir, "docker-compose.yaml")) - hasDocker := err == nil - if !hasDocker { - _, err = os.Stat(filepath.Join(b.Dir, "docker-compose.yml")) - hasDocker = err == nil - } printBanner("[WEB] Stopping Docker Containers", b.Debug) + hasDocker := false + for _, filename := range []string{"compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"} { + if _, err = os.Stat(filepath.Join(b.Dir, filename)); err == nil { + hasDocker = true + break + } + } if hasDocker { - if err := executeCommand(append(dockerComposeBin(), "down", "--remove-orphans"), b.Debug, false, nil); err != nil { + if err := executeCommand(append(dockerComposeBin(), "down", "--remove-orphans", "--volume"), b.Debug, false, nil); err != nil { return err } } else { @@ -119,7 +119,8 @@ func (b *Book) Checkout(step string) error { printBanner("[WEB] Stopping the Local Web Server", b.Debug) executeCommand([]string{"symfony", "server:stop"}, b.Debug, true, nil) - printBanner("[WEB] Stopping the Platform.sh tunnel", b.Debug) + brand := platformsh.GuessCloudFromDirectory(b.Dir) + printBanner(fmt.Sprintf("[WEB] Stopping the %s tunnel", brand.Name), b.Debug) if err := executeCommand([]string{"symfony", "tunnel:close", "-y"}, b.Debug, true, nil); err != nil { return err } @@ -196,7 +197,12 @@ func (b *Book) Checkout(step string) error { printBanner("[WEB] Installing Node dependencies (might take some time)", b.Debug) if _, err := os.Stat(filepath.Join(b.Dir, "package.json")); err == nil { - if err := executeCommand([]string{"yarn", "install"}, b.Debug, false, nil); err != nil { + args := []string{"npm", "install"} + if _, err := os.Stat(filepath.Join(b.Dir, "yarn.lock")); err == nil { + // old version of the book using Yarn instead of npm + args = []string{"yarn", "install"} + } + if err := executeCommand(args, b.Debug, false, nil); err != nil { return err } } else { @@ -205,7 +211,11 @@ func (b *Book) Checkout(step string) error { printBanner("[WEB] Building CSS and JS assets", b.Debug) if _, err := os.Stat(filepath.Join(b.Dir, "package.json")); err == nil { - if err := executeCommand([]string{"yarn", "encore", "dev"}, b.Debug, false, nil); err != nil { + args := []string{"npx", "encore", "dev"} + if _, err := os.Stat(filepath.Join(b.Dir, "yarn.lock")); err == nil { + args = []string{"yarn", "encore", "dev"} + } + if err := executeCommand(args, b.Debug, false, nil); err != nil { return err } } else { @@ -229,7 +239,12 @@ func (b *Book) Checkout(step string) error { printBanner("[SPA] Installing Node dependencies (might take some time)", b.Debug) if _, err := os.Stat(filepath.Join(b.Dir, "spa")); err == nil { os.Chdir(filepath.Join(b.Dir, "spa")) - if err := executeCommand([]string{"yarn", "install"}, b.Debug, false, nil); err != nil { + args := []string{"npm", "install"} + if _, err := os.Stat(filepath.Join(b.Dir, "yarn.lock")); err == nil { + // old version of the book using Yarn instead of npm + args = []string{"yarn", "install"} + } + if err := executeCommand(args, b.Debug, false, nil); err != nil { return err } os.Chdir(b.Dir) @@ -252,7 +267,11 @@ func (b *Book) Checkout(step string) error { } os.Chdir(filepath.Join(b.Dir, "spa")) env := append(os.Environ(), "API_ENDPOINT="+endpoint.String()) - if err := executeCommand([]string{"yarn", "encore", "dev"}, b.Debug, false, env); err != nil { + args := []string{"npx", "encore", "dev"} + if _, err := os.Stat(filepath.Join(b.Dir, "yarn.lock")); err == nil { + args = []string{"yarn", "encore", "dev"} + } + if err := executeCommand(args, b.Debug, false, env); err != nil { return err } os.Chdir(b.Dir) diff --git a/book/clone.go b/book/clone.go index 84f3c116..173be0a2 100644 --- a/book/clone.go +++ b/book/clone.go @@ -20,9 +20,12 @@ package book import ( + "encoding/json" "fmt" + "net/http" "os" "os/exec" + "strings" "github.com/pkg/errors" "github.com/symfony-cli/terminal" @@ -41,6 +44,25 @@ func (b *Book) Clone(version string) error { } ui.Section("Cloning the Repository") + + // check that version exists on Github via the API + resp, err := http.Get(fmt.Sprintf("https://api.github.com/repos/the-fast-track/book-%s", version)) + if err != nil { + return errors.Wrap(err, "unable to get version on Github") + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + versions, err := Versions() + if err != nil { + return errors.Wrap(err, "unable to get book versions") + } + terminal.Println("The version you requested does not exist; available versions:") + for _, v := range versions { + terminal.Println(fmt.Sprintf(" - %s", v)) + } + return errors.New("please choose a valid version") + } + cmd := exec.Command("git", "clone", fmt.Sprintf("https://github.com/the-fast-track/book-%s", version), b.Dir) cmd.Env = os.Environ() cmd.Stdout = os.Stdout @@ -63,3 +85,25 @@ func (b *Book) Clone(version string) error { } return nil } + +func Versions() ([]string, error) { + resp, err := http.Get("https://api.github.com/orgs/the-fast-track/repos") + if err != nil { + return nil, errors.Wrap(err, "unable to get repositories from Github") + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New("failed to get repositories from Github") + } + var repos []struct { + Name string `json:"name"` + } + if err := json.NewDecoder(resp.Body).Decode(&repos); err != nil { + return nil, errors.Wrap(err, "failed to decode response body") + } + versions := []string{} + for _, repo := range repos { + versions = append(versions, strings.Replace(repo.Name, "book-", "", 1)) + } + return versions, nil +} diff --git a/book/reqs.go b/book/reqs.go index ff4c3258..71347dce 100644 --- a/book/reqs.go +++ b/book/reqs.go @@ -63,7 +63,7 @@ func CheckRequirements() (bool, error) { } // PHP - minv, err := version.NewVersion("7.2.4") + minv, err := version.NewVersion("8.1.0") if err != nil { return false, err } @@ -81,13 +81,14 @@ func CheckRequirements() (bool, error) { terminal.Printfln("[OK] PHP installed version %s (%s)", v.FullVersion, v.PHPPath) } else { ready = false - terminal.Printfln("[KO] PHP installed; version %s found but we need version 7.2.5+ (%s)", v.FullVersion, v.PHPPath) + terminal.Printfln("[KO] PHP installed; version %s found but we need version %s+ (%s)", v.FullVersion, minv.String(), v.PHPPath) } } // PHP extensions if v != nil { exts := map[string]string{ + "iconv": "required", "json": "required", "session": "required", "ctype": "required", @@ -112,7 +113,7 @@ func CheckRequirements() (bool, error) { ready = false terminal.Printfln(`[KO] PHP extension "%s" not found, please install it - %s`, ext, reason) } else { - terminal.Printfln(`[KO] PHP extension "%s" not found, %s`, ext, reason) + terminal.Printfln(`[OK] PHP extension "%s" not found, %s`, ext, reason) } } else { terminal.Printfln(`[OK] PHP extension "%s" installed - %s`, ext, reason) @@ -146,12 +147,12 @@ func CheckRequirements() (bool, error) { terminal.Println("[OK] Docker Compose installed") } - // yarn - if _, err := exec.LookPath("yarn"); err != nil { + // NPM + if _, err := exec.LookPath("npm"); err != nil { ready = false - terminal.Println("[KO] Cannot find the Yarn package manager, please install it https://yarnpkg.com/") + terminal.Println("[KO] Cannot find the npm package manager, please install it https://www.npmjs.com/") } else { - terminal.Println("[OK] Yarn installed") + terminal.Println("[OK] npm installed") } return ready, nil diff --git a/commands/cloud_env_debug.go b/commands/cloud_env_debug.go index 872be8d1..4a46619d 100644 --- a/commands/cloud_env_debug.go +++ b/commands/cloud_env_debug.go @@ -1,3 +1,22 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + package commands import ( @@ -6,6 +25,7 @@ import ( "github.com/pkg/errors" "github.com/symfony-cli/console" + "github.com/symfony-cli/symfony-cli/local/platformsh" "github.com/symfony-cli/terminal" ) @@ -25,23 +45,29 @@ var cloudEnvDebugCmd = &console.Command{ spinner.Start() defer spinner.Stop() + psh, err := platformsh.Get() + if err != nil { + return err + } + prefix := platformsh.GuessCloudFromCommandName(c.Command.UserName).CommandPrefix + projectID := c.String("project") if projectID == "" { - out, ok := psh.RunInteractive(terminal.Logger, "", []string{"project:info", "id", "-y"}, c.Bool("debug"), nil) + out, ok := psh.RunInteractive(terminal.Logger, "", []string{prefix + "project:info", "id", "-y"}, c.Bool("debug"), nil) if !ok { return errors.New("Unable to detect the project") } projectID = strings.TrimSpace(out.String()) } - out, ok := psh.RunInteractive(terminal.Logger, "", []string{"project:info", "default_branch", "--project=" + projectID, "-y"}, c.Bool("debug"), nil) + out, ok := psh.RunInteractive(terminal.Logger, "", []string{prefix + "project:info", "default_branch", "--project=" + projectID, "-y"}, c.Bool("debug"), nil) if !ok { return errors.New("Unable to detect the default branch name") } defaultEnvName := strings.TrimSpace(out.String()) envName := c.String("environment") if envName == "" { - if out, ok := psh.RunInteractive(terminal.Logger, "", []string{"env:info", "id", "--project=" + projectID, "-y"}, false, nil); ok { + if out, ok := psh.RunInteractive(terminal.Logger, "", []string{prefix + "env:info", "id", "--project=" + projectID, "-y"}, false, nil); ok { envName = strings.TrimSpace(out.String()) } else { envName = defaultEnvName @@ -57,12 +83,12 @@ var cloudEnvDebugCmd = &console.Command{ if c.Bool("off") { terminal.Println("Deleting APP_ENV and APP_DEBUG (can take some time, --debug to tail commands)") - if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, "var:delete", "env:APP_ENV"), c.Bool("debug"), nil); !ok { + if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, prefix+"var:delete", "env:APP_ENV"), c.Bool("debug"), nil); !ok { if !strings.Contains(out.String(), "Variable not found") { return errors.New("An error occurred while removing APP_ENV") } } - if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, "var:delete", "env:APP_DEBUG"), c.Bool("debug"), nil); !ok { + if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, prefix+"var:delete", "env:APP_DEBUG"), c.Bool("debug"), nil); !ok { if !strings.Contains(out.String(), "Variable not found") { return errors.New("An error occurred while removing APP_DEBUG") } @@ -71,7 +97,7 @@ var cloudEnvDebugCmd = &console.Command{ return nil } - out, ok = psh.RunInteractive(terminal.Logger, "", []string{"project:info", "default_domain", "--project=" + projectID, "-y"}, c.Bool("debug"), nil) + out, ok = psh.RunInteractive(terminal.Logger, "", []string{prefix + "project:info", "default_domain", "--project=" + projectID, "-y"}, c.Bool("debug"), nil) if !ok { return errors.New("Unable to detect the default domain") } @@ -83,23 +109,23 @@ var cloudEnvDebugCmd = &console.Command{ } terminal.Println("Setting APP_ENV and APP_DEBUG to dev/debug (can take some time, --debug to tail commands)") - if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, "var:create", "--name=env:APP_ENV", "--value=dev"), c.Bool("debug"), nil); !ok { + if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, prefix+"var:create", "--name=env:APP_ENV", "--value=dev"), c.Bool("debug"), nil); !ok { if !strings.Contains(out.String(), "already exists on the environment") { - return errors.New("An error occurred while adding APP_ENV") + return errors.New("An error occurred while adding APP_ENV: it already exists on the environment") } - if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, "var:update", "--value=dev", "env:APP_ENV"), c.Bool("debug"), nil); !ok { + if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, prefix+"var:update", "--value=dev", "env:APP_ENV"), c.Bool("debug"), nil); !ok { if !strings.Contains(out.String(), "No changes were provided") { - return errors.New("An error occurred while adding APP_ENV") + return errors.New("An error occurred while adding APP_ENV: no changes provided") } } } - if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, "var:create", "--name=env:APP_DEBUG", "--value=1"), c.Bool("debug"), nil); !ok { + if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, prefix+"var:create", "--name=env:APP_DEBUG", "--value=1"), c.Bool("debug"), nil); !ok { if !strings.Contains(out.String(), "already exists on the environment") { - return errors.New("An error occurred while adding APP_DEBUG") + return errors.New("An error occurred while adding APP_DEBUG: it already exists on the environment") } - if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, "var:update", "--value=1", "env:APP_DEBUG"), c.Bool("debug"), nil); !ok { + if out, ok := psh.RunInteractive(terminal.Logger, "", append(defaultArgs, prefix+"var:update", "--value=1", "env:APP_DEBUG"), c.Bool("debug"), nil); !ok { if !strings.Contains(out.String(), "No changes were provided") { - return errors.New("An error occurred while adding APP_DEBUG") + return errors.New("An error occurred while adding APP_DEBUG: no changes provided") } } } diff --git a/commands/completion_others.go b/commands/completion_others.go new file mode 100644 index 00000000..1a825908 --- /dev/null +++ b/commands/completion_others.go @@ -0,0 +1,36 @@ +//go:build !darwin && !linux && !freebsd && !openbsd +// +build !darwin,!linux,!freebsd,!openbsd + +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +import ( + "github.com/posener/complete" + "github.com/symfony-cli/console" +) + +func autocompleteComposerWrapper(context *console.Context, args complete.Args) []string { + return []string{} +} + +func autocompleteSymfonyConsoleWrapper(context *console.Context, args complete.Args) []string { + return []string{} +} diff --git a/commands/completion_posix.go b/commands/completion_posix.go new file mode 100644 index 00000000..6f2e1665 --- /dev/null +++ b/commands/completion_posix.go @@ -0,0 +1,102 @@ +//go:build darwin || linux || freebsd || openbsd + +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +import ( + "embed" + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/posener/complete" + "github.com/symfony-cli/console" + "github.com/symfony-cli/symfony-cli/local/php" + "github.com/symfony-cli/terminal" +) + +// completionTemplates holds our custom shell completions templates. +// +//go:embed resources/completion.* +var completionTemplates embed.FS + +func init() { + // override console completion templates with our custom ones + console.CompletionTemplates = completionTemplates +} + +func autocompleteSymfonyConsoleWrapper(context *console.Context, words complete.Args) []string { + args := buildSymfonyAutocompleteArgs("console", words) + // Composer does not support those options yet, so we only use them for Symfony Console + args = append(args, "-a1", fmt.Sprintf("-s%s", console.GuessShell())) + + if executor, err := php.SymfonyConsoleExecutor(terminal.Logger, args); err == nil { + os.Exit(executor.Execute(false)) + } + + return []string{} +} + +func autocompleteComposerWrapper(context *console.Context, words complete.Args) []string { + args := buildSymfonyAutocompleteArgs("composer", words) + // Composer does not support multiple shell yet, so we only use the default one + args = append(args, "-sbash") + + res := php.Composer("", args, []string{}, context.App.Writer, context.App.ErrWriter, io.Discard, terminal.Logger) + os.Exit(res.ExitCode()) + + // unreachable + return []string{} +} + +func buildSymfonyAutocompleteArgs(wrappedCommand string, words complete.Args) []string { + current, err := strconv.Atoi(os.Getenv("CURRENT")) + if err != nil { + current = 1 + } else { + // we decrease one position corresponding to `symfony` command + current -= 1 + } + + args := make([]string, 0, len(words.All)) + // build the inputs command line that Symfony expects + for _, input := range words.All { + if input = strings.TrimSpace(input); input != "" { + + // remove quotes from typed values + quote := input[0] + if quote == '\'' || quote == '"' { + input = strings.TrimPrefix(input, string(quote)) + input = strings.TrimSuffix(input, string(quote)) + } + + args = append(args, fmt.Sprintf("-i%s", input)) + } + } + + return append([]string{ + "_complete", "--no-interaction", + fmt.Sprintf("-c%d", current), + fmt.Sprintf("-i%s", wrappedCommand), + }, args...) +} diff --git a/commands/data/check-requirements.php b/commands/data/check-requirements.php index 1c9bbb21..45ab9a86 100644 --- a/commands/data/check-requirements.php +++ b/commands/data/check-requirements.php @@ -372,6 +372,8 @@ class ProjectRequirements extends RequirementCollection const REQUIRED_PHP_VERSION_3x = '5.5.9'; const REQUIRED_PHP_VERSION_4x = '7.1.3'; const REQUIRED_PHP_VERSION_5x = '7.2.9'; + const REQUIRED_PHP_VERSION_6x = '8.1.0'; + const REQUIRED_PHP_VERSION_7x = '8.2.0'; public function __construct($rootDir) { @@ -386,12 +388,16 @@ public function __construct($rootDir) $rootDir = $this->getComposerRootDir($rootDir); $options = $this->readComposer($rootDir); - $phpVersion = self::REQUIRED_PHP_VERSION_3x; + $phpVersion = self::REQUIRED_PHP_VERSION_7x; if (null !== $symfonyVersion) { - if (version_compare($symfonyVersion, '5.0.0', '>=')) { + if (version_compare($symfonyVersion, '6.0.0', '>=')) { + $phpVersion = self::REQUIRED_PHP_VERSION_6x; + } elseif (version_compare($symfonyVersion, '5.0.0', '>=')) { $phpVersion = self::REQUIRED_PHP_VERSION_5x; } elseif (version_compare($symfonyVersion, '4.0.0', '>=')) { $phpVersion = self::REQUIRED_PHP_VERSION_4x; + } elseif (version_compare($symfonyVersion, '3.0.0', '>=')) { + $phpVersion = self::REQUIRED_PHP_VERSION_3x; } } @@ -929,7 +935,7 @@ private function getUploadMaxFilesize() $requirements = $symfonyRequirements->getRequirements(); // specific directory to check? -$dir = isset($args[1]) ? $args[1] : (file_exists(getcwd().'/composer.json') ? getcwd().'/composer.json' : null); +$dir = isset($args[1]) ? $args[1] : (file_exists(getcwd().'/composer.json') ? getcwd() : null); if (null !== $dir) { $projectRequirements = new ProjectRequirements($dir); $requirements = array_merge($requirements, $projectRequirements->getRequirements()); diff --git a/commands/doctrine_check_server_version_setting.go b/commands/doctrine_check_server_version_setting.go new file mode 100644 index 00000000..a2b67903 --- /dev/null +++ b/commands/doctrine_check_server_version_setting.go @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +import ( + "fmt" + + "github.com/rs/zerolog" + "github.com/symfony-cli/console" + "github.com/symfony-cli/terminal" +) + +var doctrineCheckServerVersionSettingCmd = &console.Command{ + Name: "doctrine:check-server-version-setting", + Usage: "Check if Doctrine server version is configured explicitly", + Hidden: console.Hide, + Flags: []console.Flag{ + dirFlag, + }, + Action: func(c *console.Context) error { + projectDir, err := getProjectDir(c.String("dir")) + if err != nil { + return err + } + + logger := terminal.Logger.Output(zerolog.ConsoleWriter{Out: terminal.Stderr}).With().Timestamp().Logger() + if err := checkDoctrineServerVersionSetting(projectDir, logger); err != nil { + return err + } + + fmt.Println("✅ Doctrine server version is set properly.") + return nil + }, +} diff --git a/commands/doctrine_server_version_check.go b/commands/doctrine_server_version_check.go new file mode 100644 index 00000000..0350e2e3 --- /dev/null +++ b/commands/doctrine_server_version_check.go @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +import ( + "fmt" + + "github.com/rs/zerolog" + "github.com/symfony-cli/symfony-cli/local/platformsh" +) + +// checkDoctrineServerVersionSetting checks that project has a DB and that server version is set properly +func checkDoctrineServerVersionSetting(projectDir string, logger zerolog.Logger) error { + if len(platformsh.FindLocalApplications(projectDir)) > 1 { + logger.Debug().Msg("Doctrine server version check disabled on a multiple applications project") + return nil + } + + configFile, dbName, dbVersion := platformsh.ReadDBVersionFromPlatformServiceYAML(projectDir, logger) + if dbName == "" { + // no DB + return nil + } + + errorTpl := fmt.Sprintf(` +The "%s" file defines +a "%s" version %s database service +but %%s. + +Before deploying, fix the version mismatch. + `, configFile, dbName, dbVersion) + + dotEnvVersion, err := platformsh.ReadDBVersionFromDotEnv(projectDir) + if err != nil { + return nil + } + if platformsh.DatabaseVersiondUnsynced(dotEnvVersion, dbVersion) { + return fmt.Errorf(errorTpl, fmt.Sprintf("the \".env\" file requires version %s", dotEnvVersion)) + } + + doctrineConfigVersion, err := platformsh.ReadDBVersionFromDoctrineConfigYAML(projectDir) + if err != nil { + return nil + } + if platformsh.DatabaseVersiondUnsynced(doctrineConfigVersion, dbVersion) { + return fmt.Errorf(errorTpl, fmt.Sprintf("the \"config/packages/doctrine.yaml\" file requires version %s", doctrineConfigVersion)) + } + + if dotEnvVersion == "" && doctrineConfigVersion == "" { + return fmt.Errorf(` +The "%s" file defines a "%s" database service. + +When deploying, Doctrine needs to know the database version to determine the supported SQL syntax. + +As the database is not available when Doctrine is warming up its cache on %s, +you need to explicitly set the database version in the ".env" or "config/packages/doctrine.yaml" file. + +Set the "server_version" parameter to "%s" in "config/packages/doctrine.yaml". + `, configFile, dbName, platformsh.GuessCloudFromDirectory(projectDir).Name, dbVersion) + } + + return nil +} diff --git a/commands/generator/main.go b/commands/generator/main.go new file mode 100644 index 00000000..c6e636f8 --- /dev/null +++ b/commands/generator/main.go @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/hashicorp/go-version" +) + +func main() { + generateLatestPhpVersion() +} + +func generateLatestPhpVersion() { + resp, err := http.Get("https://www.php.net/releases/active.php") + if err != nil { + panic(err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + var result map[int]map[string]struct { + Announcement bool + LatestMinor string `json:"version"` + } + + if err := json.Unmarshal(body, &result); err != nil { + panic(err) + } + + var latestVersion *version.Version + + for _, versions := range result { + for _, versionInfo := range versions { + if !versionInfo.Announcement { + continue + } + + ver, err := version.NewVersion(versionInfo.LatestMinor) + if err != nil { + panic(err) + } + + if latestVersion == nil || ver.GreaterThan(latestVersion) { + latestVersion = ver + } + } + } + + f, err := os.Create("php_version.go") + if err != nil { + panic(err) + } + f.WriteString(`// Code generated by commands/generator/main.go +// DO NOT EDIT + +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +const LatestPhpMajorVersion = "` + fmt.Sprintf("%d.%d", latestVersion.Segments()[0], latestVersion.Segments()[1]) + `" +const LatestPhpMinorVersion = "` + latestVersion.Original() + `" +`) +} diff --git a/commands/init_templating.go b/commands/init_templating.go index 859cd50a..e2b1430b 100644 --- a/commands/init_templating.go +++ b/commands/init_templating.go @@ -22,7 +22,6 @@ package commands import ( "fmt" "io" - "io/ioutil" "net/http" "net/url" "os" @@ -48,9 +47,9 @@ type nopCloser struct { func (nopCloser) Close() error { return nil } -func createRequiredFilesProject(rootDirectory, projectSlug, templateName string, minorPHPVersion string, cloudServices []*CloudService, dump, force bool) ([]string, error) { +func createRequiredFilesProject(brand platformsh.CloudBrand, rootDirectory, projectSlug, templateName string, minorPHPVersion string, cloudServices []*CloudService, dump, force bool) ([]string, error) { createdFiles := []string{} - templates, err := getTemplates(rootDirectory, templateName, minorPHPVersion) + templates, err := getTemplates(brand, rootDirectory, templateName, minorPHPVersion) if err != nil { return nil, errors.Wrap(err, "could not determine template to use") } @@ -159,7 +158,7 @@ func isValidFilePath(toTest string) bool { return true } -func getTemplates(rootDirectory, chosenTemplateName string, minorPHPVersion string) (map[string]*template.Template, error) { +func getTemplates(brand platformsh.CloudBrand, rootDirectory, chosenTemplateName string, minorPHPVersion string) (map[string]*template.Template, error) { var foundTemplate *configTemplate s := terminal.NewSpinner(terminal.Stderr) @@ -187,6 +186,9 @@ func getTemplates(rootDirectory, chosenTemplateName string, minorPHPVersion stri } } + if brand == platformsh.UpsunBrand { + directory = filepath.Join(directory, "upsun") + } if isURL, isFile := isValidURL(chosenTemplateName), isValidFilePath(chosenTemplateName); isURL || isFile { var ( templateConfigBytes []byte @@ -194,7 +196,7 @@ func getTemplates(rootDirectory, chosenTemplateName string, minorPHPVersion stri ) if isFile { - templateConfigBytes, err = ioutil.ReadFile(chosenTemplateName) + templateConfigBytes, err = os.ReadFile(chosenTemplateName) } else { var resp *http.Response resp, err = http.Get(chosenTemplateName) @@ -206,7 +208,7 @@ func getTemplates(rootDirectory, chosenTemplateName string, minorPHPVersion stri } defer resp.Body.Close() - templateConfigBytes, err = ioutil.ReadAll(resp.Body) + templateConfigBytes, err = io.ReadAll(resp.Body) } if err != nil { @@ -219,7 +221,7 @@ func getTemplates(rootDirectory, chosenTemplateName string, minorPHPVersion stri terminal.Logger.Info().Msg("Using template " + chosenTemplateName) } else { - files, err := ioutil.ReadDir(directory) + files, err := os.ReadDir(directory) if err != nil { return nil, errors.Wrap(err, "could not read configuration templates") } @@ -234,13 +236,17 @@ func getTemplates(rootDirectory, chosenTemplateName string, minorPHPVersion stri continue } + if !strings.HasSuffix(file.Name(), ".yaml") { + continue + } + templateName := strings.TrimSuffix(file.Name(), ".yaml")[strings.Index(file.Name(), "-")+1:] isTemplateChosen := chosenTemplateName == templateName if chosenTemplateName != "" && !isTemplateChosen { continue } - templateConfigBytes, err := ioutil.ReadFile(filepath.Join(directory, file.Name())) + templateConfigBytes, err := os.ReadFile(filepath.Join(directory, file.Name())) if err != nil { if isTemplateChosen { return nil, errors.Wrap(err, "could not apply configuration template") @@ -275,7 +281,7 @@ func getTemplates(rootDirectory, chosenTemplateName string, minorPHPVersion stri return nil, errors.New("no matching template found") } - phpini, err := ioutil.ReadFile(filepath.Join(directory, "php.ini")) + phpini, err := os.ReadFile(filepath.Join(directory, "php.ini")) if err != nil { return nil, errors.New("unable to find the php.ini template") } @@ -285,20 +291,27 @@ func getTemplates(rootDirectory, chosenTemplateName string, minorPHPVersion stri type: {{ $service.Type }}{{ if $service.Version }}:{{ $service.Version }}{{ end }} {{- if index $.ServiceDiskSizes $service.Type }} disk: {{ index $.ServiceDiskSizes $service.Type }} -{{ end -}} +{{ end }} {{ end -}} ` templateFuncs := getTemplateFuncs(rootDirectory, minorPHPVersion) - - templates := map[string]*template.Template{ - ".platform.app.yaml": template.Must(template.New("output").Funcs(templateFuncs).Parse(foundTemplate.Template)), - ".platform/services.yaml": template.Must(template.New("output").Funcs(templateFuncs).Parse(servicesyaml)), - ".platform/routes.yaml": template.Must(template.New("output").Funcs(templateFuncs).Parse(`"https://{all}/": { type: upstream, upstream: "{{.Slug}}:http" } + var templates map[string]*template.Template + if brand == platformsh.UpsunBrand { + templates = map[string]*template.Template{ + ".upsun/config.yaml": template.Must(template.New("output").Funcs(templateFuncs).Parse(foundTemplate.Template)), + "php.ini": template.Must(template.New("output").Funcs(templateFuncs).Parse(string(phpini))), + } + } else { + templates = map[string]*template.Template{ + ".platform.app.yaml": template.Must(template.New("output").Funcs(templateFuncs).Parse(foundTemplate.Template)), + ".platform/services.yaml": template.Must(template.New("output").Funcs(templateFuncs).Parse(servicesyaml)), + ".platform/routes.yaml": template.Must(template.New("output").Funcs(templateFuncs).Parse(`"https://{all}/": { type: upstream, upstream: "{{.Slug}}:http" } "http://{all}/": { type: redirect, to: "https://{all}/" } `)), - "php.ini": template.Must(template.New("output").Funcs(templateFuncs).Parse(string(phpini))), + "php.ini": template.Must(template.New("output").Funcs(templateFuncs).Parse(string(phpini))), + } } for path, tpl := range foundTemplate.ExtraFiles { @@ -319,7 +332,7 @@ type configTemplate struct { Template string } -func (c *configTemplate) Match(directory string, minorPHPVersion string) bool { +func (c *configTemplate) Match(directory, minorPHPVersion string) bool { for _, req := range c.Requirements { if !req.Check(directory, minorPHPVersion) { return false diff --git a/commands/init_templating_php.go b/commands/init_templating_php.go index 5b57399f..6e43494a 100644 --- a/commands/init_templating_php.go +++ b/commands/init_templating_php.go @@ -22,7 +22,7 @@ package commands import ( "encoding/json" "fmt" - "io/ioutil" + "os" "path/filepath" "strings" @@ -60,12 +60,8 @@ func hasComposerPackage(directory, pkg string) bool { return false } - if err != nil { - terminal.Logger.Warn().Msg(err.Error()) - } - if err2 != nil { - terminal.Logger.Warn().Msg(err2.Error()) - } + terminal.Logger.Warn().Msg(err.Error()) + terminal.Logger.Warn().Msg(err2.Error()) return false } @@ -136,12 +132,8 @@ func hasPHPExtension(directory, ext string) bool { return false } - if err != nil { - terminal.Logger.Warn().Msg(err.Error()) - } - if err2 != nil { - terminal.Logger.Warn().Msg(err2.Error()) - } + terminal.Logger.Warn().Msg(err.Error()) + terminal.Logger.Warn().Msg(err2.Error()) return false } @@ -158,7 +150,7 @@ type composerLock struct { } func parseComposerLock(directory string) (*composerLock, error) { - b, err := ioutil.ReadFile(filepath.Join(directory, "composer.lock")) + b, err := os.ReadFile(filepath.Join(directory, "composer.lock")) if err != nil { return nil, err } @@ -181,7 +173,7 @@ type composerJSON struct { } func parseComposerJSON(directory string) (*composerJSON, error) { - b, err := ioutil.ReadFile(filepath.Join(directory, "composer.json")) + b, err := os.ReadFile(filepath.Join(directory, "composer.json")) if err != nil { return nil, err } diff --git a/commands/init_templating_php_test.go b/commands/init_templating_php_test.go new file mode 100644 index 00000000..52167cbc --- /dev/null +++ b/commands/init_templating_php_test.go @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +import ( + "path/filepath" + "testing" +) + +var validurlcases = []string{ + "http://symfony.com", + "https://symfony.com", + "https://symfony.com/blog", + "https://symfony.com/blog?foo=bar", + "https://foo:bar@symfony.com/blog?foo=bar", +} + +var invalidurlcases = []string{ + "symfony.com", + "/Users/tucksaun/Work/src/github.com/symfony-cli/symfony-cli/cloud/init-templates/00-flex.yaml", + "c:\\windows\test.yaml", +} + +func TestIsValidUrl(t *testing.T) { + for _, test := range validurlcases { + if !isValidURL(test) { + t.Errorf("isValidUrl(%q): got false, expected true", test) + } + } + for _, test := range invalidurlcases { + if isValidURL(test) { + t.Errorf("isValidUrl(%q): got true, expected false", test) + } + } +} + +var validfilecases = []string{ + "init_templating.go", +} + +var invalidfilecases = []string{ + "../cmd", + "foo.go", + "http://symfony.com", + "https://symfony.com", + "https://symfony.com/blog", + "https://symfony.com/blog?foo=bar", + "https://foo:bar@symfony.com/blog?foo=bar", +} + +func TestIsValidFilePath(t *testing.T) { + for _, test := range validfilecases { + if !isValidFilePath(test) { + t.Errorf("isValidFilePath(%q): got false, expected true", test) + } + } + for _, test := range invalidfilecases { + if isValidFilePath(test) { + t.Errorf("isValidFilePath(%q): got true, expected false", test) + } + } +} + +func TestHasComposerPackage(t *testing.T) { + for pkg, expected := range map[string]bool{ + "foo/bar": false, + "symfony/symfony": false, + } { + result := hasComposerPackage(filepath.Join("tests", "composer_packages", "none"), pkg) + + if result != expected { + t.Errorf("hasComposerPackage(none/%q): got %v, expected %v", pkg, result, expected) + } + } + + packages := map[string]bool{ + "foo/bar": false, + "symfony/flex": true, + "symfony/dotenv": true, + } + + for _, testCase := range []string{"lock", "json", "both"} { + for pkg, expected := range packages { + result := hasComposerPackage(filepath.Join("tests", "composer_packages", testCase), pkg) + + if result != expected { + t.Errorf("hasComposerPackage(%q/%q): got %v, expected %v", testCase, pkg, result, expected) + } + } + } +} + +func TestHasPHPExtension(t *testing.T) { + for pkg, expected := range map[string]bool{ + "iconv": false, + "pdo": false, + } { + result := hasPHPExtension(filepath.Join("tests", "composer_packages", "none"), pkg) + + if result != expected { + t.Errorf("hasPHPExtension(none/%q): got %v, expected %v", pkg, result, expected) + } + } + + exts := map[string]bool{ + "pdo": false, + "iconv": true, + "ext-iconv": true, + } + + for _, testCase := range []string{"lock", "json", "both"} { + for ext, expected := range exts { + result := hasPHPExtension(filepath.Join("tests", "composer_packages", testCase), ext) + + if result != expected { + t.Errorf("hasPHPExtension(%q/%q): got %v, expected %v", testCase, ext, result, expected) + } + } + } +} diff --git a/commands/init_templating_test.go b/commands/init_templating_test.go index 52167cbc..d392c7ee 100644 --- a/commands/init_templating_test.go +++ b/commands/init_templating_test.go @@ -20,118 +20,108 @@ package commands import ( + "bytes" + "os" "path/filepath" + "strings" "testing" -) - -var validurlcases = []string{ - "http://symfony.com", - "https://symfony.com", - "https://symfony.com/blog", - "https://symfony.com/blog?foo=bar", - "https://foo:bar@symfony.com/blog?foo=bar", -} - -var invalidurlcases = []string{ - "symfony.com", - "/Users/tucksaun/Work/src/github.com/symfony-cli/symfony-cli/cloud/init-templates/00-flex.yaml", - "c:\\windows\test.yaml", -} - -func TestIsValidUrl(t *testing.T) { - for _, test := range validurlcases { - if !isValidURL(test) { - t.Errorf("isValidUrl(%q): got false, expected true", test) - } - } - for _, test := range invalidurlcases { - if isValidURL(test) { - t.Errorf("isValidUrl(%q): got true, expected false", test) - } - } -} - -var validfilecases = []string{ - "init_templating.go", -} -var invalidfilecases = []string{ - "../cmd", - "foo.go", - "http://symfony.com", - "https://symfony.com", - "https://symfony.com/blog", - "https://symfony.com/blog?foo=bar", - "https://foo:bar@symfony.com/blog?foo=bar", -} + "github.com/symfony-cli/symfony-cli/local/platformsh" +) -func TestIsValidFilePath(t *testing.T) { - for _, test := range validfilecases { - if !isValidFilePath(test) { - t.Errorf("isValidFilePath(%q): got false, expected true", test) - } - } - for _, test := range invalidfilecases { - if isValidFilePath(test) { - t.Errorf("isValidFilePath(%q): got true, expected false", test) - } +func TestCreateRequiredFilesProject(t *testing.T) { + projectDir := "./testdata/project" + slug := "slug" + services := []*CloudService{ + { + Name: "foo", + Type: "bar", + Version: "baz", + }, + { + Name: "foo1", + Type: "bar1", + Version: "baz1", + }, + { + Name: "foo2", + Type: "postgresql", + Version: "baz2", + }, } -} - -func TestHasComposerPackage(t *testing.T) { - for pkg, expected := range map[string]bool{ - "foo/bar": false, - "symfony/symfony": false, - } { - result := hasComposerPackage(filepath.Join("tests", "composer_packages", "none"), pkg) - if result != expected { - t.Errorf("hasComposerPackage(none/%q): got %v, expected %v", pkg, result, expected) - } + if _, err := createRequiredFilesProject(platformsh.PlatformshBrand, projectDir, slug, "", "8.0", services, false, true); err != nil { + panic(err) } - packages := map[string]bool{ - "foo/bar": false, - "symfony/flex": true, - "symfony/dotenv": true, + path := filepath.Join(projectDir, ".platform", "services.yaml") + result, err := os.ReadFile(path) + if err != nil { + panic(err) } - - for _, testCase := range []string{"lock", "json", "both"} { - for pkg, expected := range packages { - result := hasComposerPackage(filepath.Join("tests", "composer_packages", testCase), pkg) - - if result != expected { - t.Errorf("hasComposerPackage(%q/%q): got %v, expected %v", testCase, pkg, result, expected) - } - } + expected := ` +foo: + type: bar:baz + +foo1: + type: bar1:baz1 + +foo2: + type: postgresql:baz2 + disk: 1024 +` + result = bytes.TrimSpace(result) + expected = strings.TrimSpace(expected) + if string(result) != expected { + t.Errorf("platform/services.yaml: got %v, expected %v", string(result), expected) } } -func TestHasPHPExtension(t *testing.T) { - for pkg, expected := range map[string]bool{ - "iconv": false, - "pdo": false, - } { - result := hasPHPExtension(filepath.Join("tests", "composer_packages", "none"), pkg) - - if result != expected { - t.Errorf("hasPHPExtension(none/%q): got %v, expected %v", pkg, result, expected) - } +func TestCreateRequiredFilesProjectForUpsun(t *testing.T) { + projectDir := "./testdata/project" + slug := "slug" + services := []*CloudService{ + { + Name: "foo", + Type: "bar", + Version: "baz", + }, + { + Name: "foo1", + Type: "bar1", + Version: "baz1", + }, + { + Name: "foo2", + Type: "postgresql", + Version: "baz2", + }, } - exts := map[string]bool{ - "pdo": false, - "iconv": true, - "ext-iconv": true, + if _, err := createRequiredFilesProject(platformsh.UpsunBrand, projectDir, slug, "", "8.0", services, false, true); err != nil { + panic(err) } - for _, testCase := range []string{"lock", "json", "both"} { - for ext, expected := range exts { - result := hasPHPExtension(filepath.Join("tests", "composer_packages", testCase), ext) - - if result != expected { - t.Errorf("hasPHPExtension(%q/%q): got %v, expected %v", testCase, ext, result, expected) - } - } + path := filepath.Join(projectDir, ".upsun", "config.yaml") + result, err := os.ReadFile(path) + if err != nil { + panic(err) + } + expected := ` +services: + foo: + type: bar:baz + + foo1: + type: bar1:baz1 + + foo2: + type: postgresql:baz2 + disk: 1024 +` + result = bytes.TrimSpace(result) + expected = strings.TrimSpace(expected) + if strings.Contains(string(result), expected) { + t.Errorf("upsun/config.yaml: got %v, expected %v", string(result), expected) } } diff --git a/commands/local_check_requirements.go b/commands/local_check_requirements.go index 6671a9ce..44489a58 100644 --- a/commands/local_check_requirements.go +++ b/commands/local_check_requirements.go @@ -21,7 +21,6 @@ package commands import ( _ "embed" - "io/ioutil" "os" "path/filepath" @@ -33,6 +32,7 @@ import ( // To generate, run in symfony/requirements-checker // php bin/release.php > data/check-requirements.php +// //go:embed data/check-requirements.php var phpChecker []byte @@ -63,7 +63,7 @@ var localRequirementsCheckCmd = &console.Command{ cachePath := filepath.Join(cacheDir, "check.php") defer os.Remove(cachePath) - if err := ioutil.WriteFile(cachePath, phpChecker, 0600); err != nil { + if err := os.WriteFile(cachePath, phpChecker, 0600); err != nil { return err } diff --git a/commands/local_check_security.go b/commands/local_check_security.go index 9547fd55..41b8231d 100644 --- a/commands/local_check_security.go +++ b/commands/local_check_security.go @@ -1,3 +1,22 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + package commands import ( @@ -23,7 +42,7 @@ a specific "composer.lock" file.`, &console.StringFlag{ Name: "format", DefaultValue: "ansi", - Usage: "The output format (ansi, markdown, json, junit, or yaml)", + Usage: "The output format (ansi, text, markdown, json, junit, or yaml)", Validator: func(ctx *console.Context, format string) error { if format != "" && format != "markdown" && format != "json" && format != "yaml" && format != "ansi" && format != "junit" { return errors.Errorf(`format "%s" does not exist (supported formats: markdown, ansi, json, junit, and yaml)`, format) @@ -72,9 +91,19 @@ a specific "composer.lock" file.`, terminal.Stdout.Write(output) if os.Getenv("GITHUB_WORKSPACE") != "" { - // Ran inside a Github action, export vulns + gOutFile := os.Getenv("GITHUB_OUTPUT") + + f, err := os.OpenFile(gOutFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return console.Exit(fmt.Sprintf("unable to open github output: %s", err), 127) + } + defer f.Close() + + // Ran inside a GitHub action, export vulns output, _ := security.Format(vulns, "raw_json") - terminal.Eprintf("::set-output name=vulns::%s", output) + if _, err = f.WriteString("vulns=" + string(output) + "\n"); err != nil { + return console.Exit(fmt.Sprintf("unable to write into github output: %s", err), 127) + } } if vulns.Count() > 0 && !c.Bool(("disable-exit-code")) { diff --git a/commands/local_new.go b/commands/local_new.go index f652895c..114c600b 100644 --- a/commands/local_new.go +++ b/commands/local_new.go @@ -24,7 +24,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "os" "os/exec" @@ -65,10 +64,12 @@ var localNewCmd = &console.Command{ &console.BoolFlag{Name: "full", Usage: "Use github.com/symfony/website-skeleton (deprecated, use --webapp instead)"}, &console.BoolFlag{Name: "demo", Usage: "Use github.com/symfony/demo"}, &console.BoolFlag{Name: "webapp", Usage: "Add the webapp pack to get a fully configured web project"}, + &console.BoolFlag{Name: "api", Usage: "Add the api pack to get a fully configured api project"}, &console.BoolFlag{Name: "book", Usage: "Clone the Symfony: The Fast Track book project"}, &console.BoolFlag{Name: "docker", Usage: "Enable Docker support"}, &console.BoolFlag{Name: "no-git", Usage: "Do not initialize Git"}, - &console.BoolFlag{Name: "cloud", Usage: "Initialize Platform.sh"}, + &console.BoolFlag{Name: "upsun", Usage: "Initialize Upsun configuration"}, + &console.BoolFlag{Name: "cloud", Usage: "Initialize Platform.sh configuration"}, &console.StringSliceFlag{Name: "service", Usage: "Configure some services", Hidden: true}, &console.BoolFlag{Name: "debug", Usage: "Display commands output"}, &console.StringFlag{Name: "php", Usage: "PHP version to use"}, @@ -108,31 +109,43 @@ var localNewCmd = &console.Command{ } } + symfonyVersion := c.String("version") + if c.Bool("book") { + if symfonyVersion == "" { + versions, err := book.Versions() + if err != nil { + return errors.Wrap(err, "unable to get book versions") + } + terminal.Println("The --version flag is required for the Symfony book; available versions:") + for _, v := range versions { + terminal.Println(fmt.Sprintf(" - %s", v)) + } + return console.Exit("", 1) + } + book := &book.Book{ Dir: dir, Debug: c.Bool("debug"), Force: false, AutoConfirm: true, } - if err := book.Clone(c.String("version")); err != nil { - return err - } - return nil + + return book.Clone(symfonyVersion) } - symfonyVersion := c.String("version") if symfonyVersion != "" && c.Bool("demo") { return console.Exit("The --version flag is not supported for the Symfony Demo", 1) } - if symfonyVersion == "" && c.Bool("book") { - return console.Exit("The --version flag is required for the Symfony book", 1) + if c.Bool("webapp") && c.Bool("api") { + return console.Exit("The --api flag cannot be used with --webapp", 1) } - if c.Bool("webapp") && c.Bool("no-git") { - return console.Exit("The --webapp flag cannot be used with --no-git", 1) + withCloud := c.Bool("cloud") || c.Bool("upsun") + if len(c.StringSlice("service")) > 0 && !withCloud { + return console.Exit("The --service flag cannot be used without --cloud or --upsun", 1) } - if len(c.StringSlice("service")) > 0 && !c.Bool("cloud") { - return console.Exit("The --service flag cannot be used without --cloud", 1) + if withCloud && c.Bool("no-git") { + return console.Exit("The --no-git flag cannot be used with --cloud or --upsun", 1) } s := terminal.NewSpinner(terminal.Stderr) @@ -148,7 +161,7 @@ var localNewCmd = &console.Command{ return err } - if "" != c.String("php") && !c.Bool("cloud") { + if c.String("php") != "" && !withCloud { if err := createPhpVersionFile(c.String("php"), dir); err != nil { return err } @@ -156,7 +169,7 @@ var localNewCmd = &console.Command{ if !c.Bool("no-git") { if _, err := exec.LookPath("git"); err == nil { - if err := initProjectGit(c, s, dir); err != nil { + if err := initProjectGit(c, dir); err != nil { return err } } @@ -165,15 +178,22 @@ var localNewCmd = &console.Command{ if c.Bool("webapp") { if err := runComposer(c, dir, []string{"require", "webapp"}, c.Bool("debug")); err != nil { return err + } else if !c.Bool("no-git") { + buf, err := git.AddAndCommit(dir, []string{"."}, "Add webapp packages", c.Bool("debug")) + if err != nil { + fmt.Print(buf.String()) + return err + } } - buf, err := git.AddAndCommit(dir, []string{"."}, "Add webapp packages", c.Bool("debug")) - if err != nil { - fmt.Print(buf.String()) + } + + if c.Bool("api") { + if err := runComposer(c, dir, []string{"require", "api"}, c.Bool("debug")); err != nil { return err } } - if c.Bool("cloud") { + if withCloud { if err := runComposer(c, dir, []string{"require", "platformsh"}, c.Bool("debug")); err != nil { return err } @@ -182,7 +202,11 @@ var localNewCmd = &console.Command{ fmt.Print(buf.String()) return err } - if err := initCloud(c, s, minorPHPVersion, dir); err != nil { + brand := platformsh.PlatformshBrand + if c.Bool("upsun") { + brand = platformsh.UpsunBrand + } + if err := initCloud(c, brand, minorPHPVersion, dir); err != nil { return err } } @@ -211,8 +235,8 @@ func isEmpty(dir string) (bool, error) { return false, err } -func initCloud(c *console.Context, s *terminal.Spinner, minorPHPVersion, dir string) error { - terminal.Println("* Adding Platform.sh configuration") +func initCloud(c *console.Context, brand platformsh.CloudBrand, minorPHPVersion, dir string) error { + terminal.Printfln("* Adding %s configuration", brand) cloudServices, err := parseCloudServices(dir, c.StringSlice("service")) if err != nil { @@ -220,12 +244,12 @@ func initCloud(c *console.Context, s *terminal.Spinner, minorPHPVersion, dir str } // FIXME: display or hide output based on debug flag - _, err = createRequiredFilesProject(dir, "app", "", minorPHPVersion, cloudServices, c.Bool("dump"), c.Bool("force")) + _, err = createRequiredFilesProject(brand, dir, "app", "", minorPHPVersion, cloudServices, c.Bool("dump"), c.Bool("force")) if err != nil { return err } - buf, err := git.AddAndCommit(dir, []string{"."}, "Add Platform.sh configuration", c.Bool("debug")) + buf, err := git.AddAndCommit(dir, []string{"."}, fmt.Sprintf("Add %s configuration", brand), c.Bool("debug")) if err != nil { fmt.Print(buf.String()) } @@ -270,7 +294,7 @@ func parseCLIServices(services []string) ([]*CloudService, error) { func parseDockerComposeServices(dir string) []*CloudService { var cloudServices []*CloudService - options, err := compose.NewProjectOptions(nil, compose.WithWorkingDirectory(dir), compose.WithDefaultConfigPath, compose.WithConfigFileEnv) + options, err := compose.NewProjectOptions(nil, compose.WithWorkingDirectory(dir), compose.WithDefaultConfigPath, compose.WithConfigFileEnv, compose.WithEnv(os.Environ())) if err != nil { return nil } @@ -278,6 +302,8 @@ func parseDockerComposeServices(dir string) []*CloudService { if err != nil { return nil } + + seen := map[string]bool{} for _, service := range project.Services { for _, port := range service.Ports { var s *CloudService @@ -298,12 +324,18 @@ func parseDockerComposeServices(dir string) []*CloudService { } else if port.Target == 9092 { s = &CloudService{Type: "kafka"} } - if s != nil { + _, done := seen[service.Name] + if s != nil && !done { + seen[service.Name] = true s.Name = service.Name parts := strings.Split(service.Image, ":") s.Version = regexp.MustCompile(`\d+(\.\d+)?`).FindString(parts[len(parts)-1]) + serviceLastVersion := platformsh.ServiceLastVersion(s.Type) if s.Version == "" { - s.Version = platformsh.ServiceLastVersion(s.Type) + s.Version = serviceLastVersion + } else if s.Version > serviceLastVersion { + terminal.Printf("Unsupported %s version %s using version %s\n", s.Type, s.Version, serviceLastVersion) + s.Version = serviceLastVersion } cloudServices = append(cloudServices, s) } @@ -312,10 +344,12 @@ func parseDockerComposeServices(dir string) []*CloudService { return cloudServices } -func initProjectGit(c *console.Context, s *terminal.Spinner, dir string) error { +func initProjectGit(c *console.Context, dir string) error { terminal.Println("* Setting up the project under Git version control") terminal.Printfln(" (running git init %s)\n", dir) - if buf, err := git.Init(dir, c.Bool("debug")); err != nil { + // Only force the branch to be "main" when running a Cloud context to make + // onboarding simpler. + if buf, err := git.Init(dir, c.Bool("cloud") || c.Bool("upsun"), c.Bool("debug")); err != nil { fmt.Print(buf.String()) return err } @@ -391,7 +425,7 @@ func getSpecialVersion(version string) (string, error) { } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return "", err } diff --git a/commands/local_new_test.go b/commands/local_new_test.go new file mode 100644 index 00000000..81a9e8f0 --- /dev/null +++ b/commands/local_new_test.go @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +import ( + "os" + "strconv" + "testing" + + "github.com/symfony-cli/symfony-cli/local/platformsh" +) + +func TestParseDockerComposeServices(t *testing.T) { + lastVersion := platformsh.ServiceLastVersion("postgresql") + + if n, err := strconv.Atoi(lastVersion); err != nil { + t.Error("Could not generate test cases:", err) + } else { + os.Setenv("POSTGRES_NEXT_VERSION", strconv.Itoa(n+1)) + defer os.Unsetenv("POSTGRES_NEXT_VERSION") + } + + for dir, expected := range map[string]CloudService{ + "testdata/docker/postgresql/noversion/": { + Name: "database", + Type: "postgresql", + Version: lastVersion, + }, + "testdata/docker/postgresql/10/": { + Name: "database", + Type: "postgresql", + Version: "10", + }, + "testdata/docker/postgresql/next/": { + Name: "database", + Type: "postgresql", + Version: lastVersion, + }, + } { + result := parseDockerComposeServices(dir) + if result[0].Version != expected.Version { + t.Errorf("parseDockerComposeServices(none/%q): got %v, expected %v", dir, result[0].Version, expected.Version) + } + } +} diff --git a/commands/local_php_list.go b/commands/local_php_list.go index 91d58382..1c76d713 100644 --- a/commands/local_php_list.go +++ b/commands/local_php_list.go @@ -19,6 +19,8 @@ package commands +//go:generate go run generator/main.go + import ( "os" "strings" @@ -88,8 +90,9 @@ var localPhpListCmd = &console.Command{ } terminal.Println("") - terminal.Println("To control the version used in a directory, create a .php-version file that contains the version number (e.g. 7.2 or 7.2.15).") - terminal.Println("If you're using Platform.sh, the version can also be specified in the .platform.app.yaml file.") + terminal.Println("To control the version used in a directory, create a .php-version file that contains the version number (e.g. " + LatestPhpMajorVersion + " or " + LatestPhpMinorVersion + "),") + terminal.Println("or define config.platform.php inside composer.json.") + terminal.Println("If you're using Platform.sh or Upsun, the version can also be specified in their configuration files.") return nil }, diff --git a/commands/local_proxy_start.go b/commands/local_proxy_start.go index 50182da9..47f5d8b4 100644 --- a/commands/local_proxy_start.go +++ b/commands/local_proxy_start.go @@ -134,7 +134,7 @@ var localProxyStartCmd = &console.Command{ logger = zerolog.New(lw).With().Timestamp().Logger() } - proxy := proxy.New(config, ca, log.New(logger, "", 0), terminal.GetLogLevel() >= 5) + proxy := proxy.New(config, ca, log.New(logger, "", 0), terminal.IsDebug()) errChan := make(chan error) go func() { errChan <- proxy.Start() @@ -167,7 +167,7 @@ var localProxyStartCmd = &console.Command{ shutdownCh := make(chan bool, 1) go func() { sigsCh := make(chan os.Signal, 1) - signal.Notify(sigsCh, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) + signal.Notify(sigsCh, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM) <-sigsCh signal.Stop(sigsCh) shutdownCh <- true diff --git a/commands/local_proxy_tld.go b/commands/local_proxy_tld.go new file mode 100644 index 00000000..0aa5b630 --- /dev/null +++ b/commands/local_proxy_tld.go @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +import ( + "fmt" + "regexp" + + "github.com/symfony-cli/console" + "github.com/symfony-cli/symfony-cli/local/proxy" + "github.com/symfony-cli/symfony-cli/util" + "github.com/symfony-cli/terminal" +) + +var localProxyTLD = &console.Command{ + Category: "local", + Name: "proxy:tld", + Aliases: []*console.Alias{{Name: "proxy:tld"}, {Name: "proxy:change:tld"}}, + Usage: "Display or change the TLD for the proxy", + Args: []*console.Arg{ + {Name: "tld", Description: "The TLD for the project proxy", Optional: true}, + }, + Action: func(c *console.Context) error { + homeDir := util.GetHomeDir() + config, err := proxy.Load(homeDir) + if err != nil { + return err + } + + if c.Args().Present() { + config.TLD = c.Args().Get("tld") + if !regexp.MustCompile(`^[a-z]{1,63}$`).MatchString(config.TLD) { + return fmt.Errorf("the TLD must only contain lowercase letters") + } + if err = config.Save(); err != nil { + return err + } + } + + terminal.Printfln("The proxy is configured with the following TLD: %s", config.TLD) + return nil + }, +} diff --git a/commands/local_proxy_url.go b/commands/local_proxy_url.go new file mode 100644 index 00000000..3da02596 --- /dev/null +++ b/commands/local_proxy_url.go @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +import ( + "errors" + "fmt" + + "github.com/symfony-cli/console" + "github.com/symfony-cli/symfony-cli/local/pid" + "github.com/symfony-cli/terminal" +) + +var localProxyUrlCmd = &console.Command{ + Category: "local", + Name: "proxy:url", + Aliases: []*console.Alias{{Name: "proxy:url"}}, + Usage: "Get the local proxy server URL", + Description: `Get the local proxy server URL, for example if you need to define HTTP_PROXY/HTTPS_PROXY environment variables +when running an external program: + +e.g. with Blackfire: + HTTP_PROXY=$(symfony proxy:url) HTTPS_PROXY=$(symfony proxy:url) blackfire curl ... + +e.g. with Cypress: + HTTP_PROXY=$(symfony proxy:url) HTTPS_PROXY=$(symfony proxy:url) ./node_modules/bin/cypress ... +`, + Action: func(c *console.Context) error { + pidFile := pid.New("__proxy__", nil) + if !pidFile.IsRunning() { + return errors.New("the proxy server is not running") + } + + url := fmt.Sprintf("%s://127.0.0.1:%d", pidFile.Scheme, pidFile.Port) + terminal.Print(url) + + return nil + }, +} diff --git a/commands/local_server_list.go b/commands/local_server_list.go index 4b7dbdac..55e972a8 100644 --- a/commands/local_server_list.go +++ b/commands/local_server_list.go @@ -34,7 +34,7 @@ import ( var localServerListCmd = &console.Command{ Category: "local", Name: "server:list", - Aliases: []*console.Alias{{Name: "server:list"}}, + Aliases: []*console.Alias{{Name: "server:list"}, {Name: "server:ls"}}, Usage: "List all configured local web servers", Action: func(c *console.Context) error { return printConfiguredServers() @@ -50,7 +50,7 @@ func printConfiguredServers() error { if err != nil { return errors.WithStack(err) } - runningProjects, err := pid.ToConfiguredProjects() + runningProjects, err := pid.ToConfiguredProjects(true) if err != nil { return errors.WithStack(err) } diff --git a/commands/local_server_start.go b/commands/local_server_start.go index 222e7a1f..1ddb5016 100644 --- a/commands/local_server_start.go +++ b/commands/local_server_start.go @@ -29,6 +29,7 @@ import ( "os/exec" "os/signal" "path/filepath" + "sync" "syscall" "github.com/pkg/errors" @@ -50,6 +51,8 @@ import ( ) var localWebServerProdWarningMsg = "The local web server is optimized for local development and MUST never be used in a production setup." +var localWebServerTlsKeyLogWarningMsg = "Logging TLS master key is enabled. It means TLS connections between the client and this server will be INSECURE. This is NOT recommended unless you are debugging the connections." +var localWebServerAllowsCORSLogWarningMsg = "Cross-origin resource sharing (CORS) is enabled for all requests.\nYou may want to use https://github.com/nelmio/NelmioCorsBundle to have better control over HTTP headers." var localServerStartCmd = &console.Command{ Category: "local", @@ -63,10 +66,25 @@ var localServerStartCmd = &console.Command{ &console.StringFlag{Name: "document-root", Usage: "Project document root (auto-configured by default)"}, &console.StringFlag{Name: "passthru", Usage: "Project passthru index (auto-configured by default)"}, &console.IntFlag{Name: "port", DefaultValue: 8000, Usage: "Preferred HTTP port"}, + &console.StringFlag{Name: "listen-ip", DefaultValue: "127.0.0.1", Usage: "The IP on which the CLI should listen"}, + &console.BoolFlag{Name: "allow-all-ip", Usage: "Listen on all the available interfaces"}, &console.BoolFlag{Name: "daemon", Aliases: []string{"d"}, Usage: "Run the server in the background"}, &console.BoolFlag{Name: "no-humanize", Usage: "Do not format JSON logs"}, &console.StringFlag{Name: "p12", Usage: "Name of the file containing the TLS certificate to use in p12 format"}, &console.BoolFlag{Name: "no-tls", Usage: "Use HTTP instead of HTTPS"}, + &console.BoolFlag{Name: "use-gzip", Usage: "Use GZIP"}, + &console.StringFlag{ + Name: "tls-key-log-file", + Usage: "Destination for TLS master secrets in NSS key log format", + // If 'SSLKEYLOGFILE' environment variable is set, uses this as a + // destination of TLS key log. In this context, the name + // 'SSLKEYLOGFILE' is common, so using 'SSL' instead of 'TLS' name. + // This environment variable is preferred than the key log file + // from the console argument. + EnvVars: []string{"SSLKEYLOGFILE"}, + }, + &console.BoolFlag{Name: "no-workers", Usage: "Do not start workers"}, + &console.BoolFlag{Name: "allow-cors", Usage: "Allow Cross-origin resource sharing (CORS) requests"}, }, Action: func(c *console.Context) error { ui := terminal.SymfonyStyle(terminal.Stdout, terminal.Stdin) @@ -75,7 +93,7 @@ var localServerStartCmd = &console.Command{ return err } pidFile := pid.New(projectDir, nil) - pidFile.CustomName = "Web Server" + pidFile.CustomName = pid.WebServerName if pidFile.IsRunning() { ui.Warning("The local web server is already running") return errors.WithStack(printWebServerStatus(projectDir)) @@ -86,16 +104,19 @@ var localServerStartCmd = &console.Command{ homeDir := util.GetHomeDir() - shutdownCh := make(chan bool, 1) - go func() { - sigsCh := make(chan os.Signal, 1) - signal.Notify(sigsCh, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) - <-sigsCh - signal.Stop(sigsCh) - shutdownCh <- true - }() + if err := reexec.NotifyForeground("boot"); err != nil { + terminal.Logger.Error().Msg("Unable to go to the background: %s.\nAborting\n" + err.Error()) + return console.Exit("", 1) + } - if c.Bool("daemon") && !reexec.IsChild() { + reexec.NotifyForeground("config") + config, fileConfig, err := project.NewConfigFromContext(c, projectDir) + if err != nil { + return errors.WithStack(err) + } + config.HomeDir = homeDir + + if config.Daemon && !reexec.IsChild() { varDir := filepath.Join(homeDir, "var") if err := os.MkdirAll(varDir, 0755); err != nil { return errors.Wrap(err, "Could not create status file") @@ -106,31 +127,28 @@ var localServerStartCmd = &console.Command{ } terminal.Eprintln("Impossible to go to the background") terminal.Eprintln("Continue in foreground") - c.Set("daemon", "false") + config.Daemon = false } else { terminal.Eprintfln("Stream the logs via %s server:log", c.App.HelpName) return nil } } - if err := reexec.NotifyForeground("boot"); err != nil { - terminal.Logger.Error().Msg("Unable to go to the background: %s.\nAborting\n" + err.Error()) - return console.Exit("", 1) - } - - reexec.NotifyForeground("config") - config, fileConfig, err := project.NewConfigFromContext(c, projectDir) - if err != nil { - return errors.WithStack(err) - } - config.HomeDir = homeDir + shutdownCh := make(chan bool) + go func() { + sigsCh := make(chan os.Signal, 10) + signal.Notify(sigsCh, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM) + <-sigsCh + signal.Stop(sigsCh) + shutdownCh <- true + }() reexec.NotifyForeground("proxy") proxyConfig, err := proxy.Load(homeDir) if err != nil { return errors.WithStack(err) } - if fileConfig != nil { + if fileConfig != nil && fileConfig.Proxy != nil { if err := proxyConfig.ReplaceDirDomains(projectDir, fileConfig.Proxy.Domains); err != nil { return errors.WithStack(err) } @@ -168,6 +186,14 @@ var localServerStartCmd = &console.Command{ } } + if config.TlsKeyLogFile != "" { + ui.Warning(localWebServerTlsKeyLogWarningMsg) + } + + if config.AllowCORS { + ui.Warning(localWebServerAllowsCORSLogWarningMsg) + } + lw, err := pidFile.LogWriter() if err != nil { return err @@ -284,6 +310,10 @@ var localServerStartCmd = &console.Command{ reexec.NotifyForeground("listening") ui.Warning(localWebServerProdWarningMsg) + if config.ListenIp == "127.0.0.1" { + ui.Warning(`Please note that the Symfony CLI only listens on 127.0.0.1 by default since version 5.10.3. + You can use the --allow-all-ip or --listen-ip flags to change this behavior.`) + } ui.Success(msg) } @@ -291,8 +321,15 @@ var localServerStartCmd = &console.Command{ go tailer.Tail(terminal.Stderr) } - if fileConfig != nil { + if fileConfig != nil && !config.NoWorkers { reexec.NotifyForeground("workers") + + _, isDockerComposeWorkerConfigured := fileConfig.Workers[project.DockerComposeWorkerKey] + var dockerWg sync.WaitGroup + if isDockerComposeWorkerConfigured { + dockerWg.Add(1) + } + for name, worker := range fileConfig.Workers { pidFile := pid.New(projectDir, worker.Cmd) if pidFile.IsRunning() { @@ -300,6 +337,7 @@ var localServerStartCmd = &console.Command{ continue } pidFile.Watched = worker.Watch + pidFile.CustomName = name // we run each worker in its own goroutine for several reasons: // * to get things up and running faster @@ -319,10 +357,43 @@ var localServerStartCmd = &console.Command{ runner.BuildCmdHook = func(cmd *exec.Cmd) error { cmd.Env = append(cmd.Env, envs.AsSlice(env)...) - return nil } + if name == project.DockerComposeWorkerKey { + originalBuildCmdHook := runner.BuildCmdHook + + runner.BuildCmdHook = func(cmd *exec.Cmd) error { + cmd.Args = append(cmd.Args, "--wait") + + return originalBuildCmdHook(cmd) + } + + runner.SuccessHook = func(runner *local.Runner, cmd *exec.Cmd) { + terminal.Eprintln("INFO Docker Compose is now up, switching to non detached mode") + + // set up the worker for an immediate restart so + // that it starts monitoring the containers as soon + // as possible after the initial startup + runner.AlwaysRestartOnExit = true + // but next time this process is successful we don't + // have to do anything specific + runner.SuccessHook = nil + // and we move back AlwaysRestartOnExit to false + + runner.BuildCmdHook = func(cmd *exec.Cmd) error { + runner.AlwaysRestartOnExit = false + + return originalBuildCmdHook(cmd) + } + + dockerWg.Done() + } + } else if isDockerComposeWorkerConfigured { + terminal.Eprintfln("INFO Worker \"%s\" waiting for Docker Compose to be up", name) + dockerWg.Wait() + } + ui.Success(fmt.Sprintf("Started worker \"%s\"", name)) if err := runner.Run(); err != nil { terminal.Eprintfln("WARNING Worker \"%s\" exited with an error: %s", name, err) @@ -341,8 +412,14 @@ var localServerStartCmd = &console.Command{ return err case <-shutdownCh: terminal.Eprintln("") - terminal.Eprintln("Shutting down!") - if err := cleanupWebServerFiles(projectDir, pidFile); err != nil { + terminal.Eprintln("Shutting down! Waiting for all workers to be done.") + err := waitForWorkers(projectDir, pidFile) + // wait for the PHP Server to be done cleaning up + if p.PHPServer != nil { + <-p.PHPServer.StoppedChan + } + pidFile.CleanupDirectories() + if err != nil { return err } terminal.Eprintln("") @@ -368,3 +445,22 @@ func cleanupWebServerFiles(projectDir string, pidFile *pid.PidFile) error { } return nil } + +func waitForWorkers(projectDir string, pidFile *pid.PidFile) error { + pids := pid.AllWorkers(projectDir) + if len(pids) < 1 { + return nil + } + + var g errgroup.Group + for _, p := range pids { + g.Go(p.WaitForExit) + } + if err := g.Wait(); err != nil { + return err + } + if err := pidFile.Remove(); err != nil { + return err + } + return nil +} diff --git a/commands/local_server_status.go b/commands/local_server_status.go index 13936caf..7ea06454 100644 --- a/commands/local_server_status.go +++ b/commands/local_server_status.go @@ -103,7 +103,7 @@ func printWebServerStatus(projectDir string) error { env := envs.AsMap(data) envVars := `None` if env["SYMFONY_TUNNEL"] != "" && env["SYMFONY_TUNNEL_ENV"] != "" { - envVars = `Exposed from Platform.sh` + envVars = fmt.Sprintf(`Exposed from %s`, env["SYMFONY_TUNNEL_BRAND"]) } if env["SYMFONY_DOCKER_ENV"] == "1" && env["SYMFONY_TUNNEL_ENV"] == "" { envVars = `Exposed from Docker` diff --git a/commands/local_server_stop.go b/commands/local_server_stop.go index 39708f1e..591566f4 100644 --- a/commands/local_server_stop.go +++ b/commands/local_server_stop.go @@ -21,6 +21,7 @@ package commands import ( "fmt" + "os" "github.com/symfony-cli/console" "github.com/symfony-cli/symfony-cli/local/pid" @@ -35,37 +36,112 @@ var localServerStopCmd = &console.Command{ Usage: "Stop the local web server", Flags: []console.Flag{ dirFlag, + &console.BoolFlag{Name: "all", Usage: "Stop all local web servers"}, }, Action: func(c *console.Context) error { - projectDir, err := getProjectDir(c.String("dir")) + if c.Bool("all") && c.IsSet("dir") { + return fmt.Errorf("you cannot use the --all option with a specific directory") + } + + var dirs []string + + if c.Bool("all") { + configuredAndRunning, err := pid.ToConfiguredProjects(false) + if err != nil { + return err + } + + for dir := range configuredAndRunning { + dirs = append(dirs, dir) + } + } else { + projectDir, err := getProjectDir(c.String("dir")) + if err != nil { + return err + } + + dirs = append(dirs, projectDir) + } + + return stopProjects(dirs, c.Bool("all")) + }, +} + +func stopProjects(dirs []string, allFlag bool) error { + ui := terminal.SymfonyStyle(terminal.Stdout, terminal.Stdin) + running := 0 + + if len(dirs) == 0 { + ui.Success("No local web servers to stop") + + return nil + } + + for _, dir := range dirs { + projectDir, err := getProjectDir(dir) + runningProcessesForProject := 0 if err != nil { return err } - ui := terminal.SymfonyStyle(terminal.Stdout, terminal.Stdin) + + if allFlag { + ui.Section(fmt.Sprintf("Stopping project %s", projectDir)) + } + webserver := pid.New(projectDir, nil) pids := append(pid.AllWorkers(projectDir), webserver) var g errgroup.Group - running := 0 + for _, p := range pids { + if !p.IsRunning() { + continue + } + + running++ + runningProcessesForProject++ + g.Go(p.WaitForExit) + + // we first notify the webserver in order to let it know it should + // not restart any workers anymore + if p.CustomName == pid.WebServerName { + p.Signal(os.Interrupt) + continue + } + } + + if runningProcessesForProject == 0 { + ui.Success("The web server is not running") + continue + } + for _, p := range pids { terminal.Printf("Stopping %s", p.ShortName()) - if p.IsRunning() { - running++ - g.Go(p.Stop) - terminal.Println("") - } else { + if !p.IsRunning() { terminal.Println(": not running") + continue + } + + // we don't "stop" the webserver because it acts as a monitoring + // process and as such we already signaled it earlier (see previous + // loop). If we do, the signal would be broadcast to the full + // process group, breaking some workers (as docker compose for + // example) because they would receive too many signals for a single + // stop request. + if p.CustomName == pid.WebServerName { + } else if err := p.Stop(); err != nil { + terminal.Printf(": %s", err) } + terminal.Println("") } terminal.Println("") if err := g.Wait(); err != nil { return err } - if running == 0 { - ui.Success("The web server is not running") - } else { - ui.Success(fmt.Sprintf("Stopped %d process(es) successfully", running)) - } - return nil - }, + } + + if running > 0 { + ui.Success(fmt.Sprintf("Stopped %d process(es) successfully", running)) + } + + return nil } diff --git a/commands/openers.go b/commands/openers.go index ac90be00..f8afbdeb 100644 --- a/commands/openers.go +++ b/commands/openers.go @@ -27,19 +27,11 @@ import ( "github.com/symfony-cli/console" "github.com/symfony-cli/symfony-cli/envs" "github.com/symfony-cli/symfony-cli/local/pid" + "github.com/symfony-cli/symfony-cli/local/proxy" + "github.com/symfony-cli/symfony-cli/util" "github.com/symfony-cli/terminal" ) -var openDocCmd = &console.Command{ - Category: "open", - Name: "docs", - Usage: "Open the online Web documentation", - Action: func(c *console.Context) error { - abstractOpenCmd("https://symfony.com/doc/cloud") - return nil - }, -} - var projectLocalOpenCmd = &console.Command{ Category: "open", Name: "local", @@ -60,9 +52,16 @@ var projectLocalOpenCmd = &console.Command{ if !pidFile.IsRunning() { return console.Exit("Local web server is down.", 1) } - abstractOpenCmd(fmt.Sprintf("%s://127.0.0.1:%d/%s", + host := fmt.Sprintf("127.0.0.1:%d", pidFile.Port) + if proxyConf, err := proxy.Load(util.GetHomeDir()); err == nil { + domains := proxyConf.GetReachableDomains(projectDir) + if len(domains) > 0 { + host = domains[0] + } + } + abstractOpenCmd(fmt.Sprintf("%s://%s/%s", pidFile.Scheme, - pidFile.Port, + host, strings.TrimLeft(c.String("path"), "/"), )) return nil @@ -85,9 +84,7 @@ var projectLocalMailCatcherOpenCmd = &console.Command{ if err != nil { return err } - prefix := env.FindRelationshipPrefix("mailer", "http") - values := envs.AsMap(env) - url, exists := values[prefix+"URL"] + url, exists := env.FindServiceUrl("mailer") if !exists { return console.Exit("Mailcatcher Web interface not found", 1) } @@ -112,9 +109,7 @@ var projectLocalRabbitMQManagementOpenCmd = &console.Command{ if err != nil { return err } - prefix := env.FindRelationshipPrefix("amqp", "http") - values := envs.AsMap(env) - url, exists := values[prefix+"URL"] + url, exists := env.FindServiceUrl("amqp") if !exists { return console.Exit("RabbitMQ management not found", 1) } @@ -123,10 +118,40 @@ var projectLocalRabbitMQManagementOpenCmd = &console.Command{ }, } +var projectLocalServiceOpenCmd = &console.Command{ + Category: "open", + Name: "local:service", + Usage: "Open a local service web interface in a browser", + Flags: []console.Flag{ + dirFlag, + }, + Args: []*console.Arg{ + {Name: "service", Description: "The service name (or type) to open"}, + }, + Action: func(c *console.Context) error { + projectDir, err := getProjectDir(c.String("dir")) + if err != nil { + return err + } + env, err := envs.NewLocal(projectDir, terminal.IsDebug()) + if err != nil { + return err + } + service := c.Args().Get("service") + url, exists := env.FindServiceUrl(service) + if !exists { + return console.Exit(fmt.Sprintf("Service \"%s\" not found", service), 1) + } + + abstractOpenCmd(url) + return nil + }, +} + func abstractOpenCmd(url string) { if err := open.Run(url); err != nil { terminal.Eprintln("Error while opening:", err, "") - terminal.Eprintfln("Please visit %s manually.", url) + terminal.Eprintfln("Please visit %s manually.", url, url) } else { terminal.Eprintfln("Opened: %s", url, url) } diff --git a/commands/php_version.go b/commands/php_version.go new file mode 100644 index 00000000..7df9b51a --- /dev/null +++ b/commands/php_version.go @@ -0,0 +1,26 @@ +// Code generated by commands/generator/main.go +// DO NOT EDIT + +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +const LatestPhpMajorVersion = "8.4" +const LatestPhpMinorVersion = "8.4.7" diff --git a/commands/platformsh.go b/commands/platformsh.go deleted file mode 100644 index 1b835f74..00000000 --- a/commands/platformsh.go +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (c) 2021-present Fabien Potencier - * - * This file is part of Symfony CLI project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package commands - -import ( - "bytes" - _ "embed" - "io" - "os" - "path/filepath" - "strings" - - "github.com/mitchellh/go-homedir" - "github.com/rs/zerolog" - "github.com/symfony-cli/console" - "github.com/symfony-cli/symfony-cli/local/php" - "github.com/symfony-cli/symfony-cli/local/platformsh" - "github.com/symfony-cli/symfony-cli/util" -) - -type platformshCLI struct { - Commands []*console.Command - - path string -} - -func NewPlatformShCLI() (*platformshCLI, error) { - home, err := homedir.Dir() - if err != nil { - return nil, err - } - p := &platformshCLI{ - path: filepath.Join(home, ".platformsh", "bin", "platform"), - } - for _, command := range platformsh.Commands { - command.Action = p.proxyPSHCmd(strings.TrimPrefix(command.Category+":"+command.Name, "cloud:")) - command.Args = []*console.Arg{ - {Name: "anything", Slice: true, Optional: true}, - } - command.Flags = append(command.Flags, - &console.BoolFlag{Name: "no", Aliases: []string{"n"}}, - &console.BoolFlag{Name: "yes", Aliases: []string{"y"}}, - ) - if _, ok := platformshBeforeHooks[command.FullName()]; !ok { - // do not parse flags if we don't have hooks - command.FlagParsing = console.FlagParsingSkipped - } - p.Commands = append(p.Commands, command) - } - return p, nil -} - -func (p *platformshCLI) PSHMainCommands() []*console.Command { - names := map[string]bool{ - "cloud:project:list": true, - "cloud:environment:list": true, - "cloud:environment:branch": true, - "cloud:tunnel:open": true, - "cloud:environment:ssh": true, - "cloud:environment:push": true, - "cloud:domain:list": true, - "cloud:variable:list": true, - "cloud:user:add": true, - } - mainCmds := []*console.Command{} - for _, command := range p.Commands { - if names[command.FullName()] { - mainCmds = append(mainCmds, command) - } - } - return mainCmds -} - -func (p *platformshCLI) proxyPSHCmd(commandName string) console.ActionFunc { - return func(commandName string) console.ActionFunc { - return func(c *console.Context) error { - // the Platform.sh CLI is always available on the containers thanks to the configurator - if !util.InCloud() { - home, err := homedir.Dir() - if err != nil { - return err - } - if err := php.InstallPlatformPhar(home); err != nil { - return console.Exit(err.Error(), 1) - } - } - - if hook, ok := platformshBeforeHooks["cloud:"+commandName]; ok && !console.IsHelp(c) { - if err := hook(c); err != nil { - return err - } - } - - args := os.Args[1:] - for i := range args { - if args[i] == c.Command.UserName { - args[i] = commandName - break - } - } - e := p.executor(args) - return console.Exit("", e.Execute(false)) - } - }(commandName) -} - -func (p *platformshCLI) executor(args []string) *php.Executor { - env := []string{ - "PLATFORMSH_CLI_APPLICATION_NAME=Platform.sh CLI for Symfony", - "PLATFORMSH_CLI_APPLICATION_EXECUTABLE=symfony", - "XDEBUG_MODE=off", - } - if util.InCloud() { - env = append(env, "PLATFORMSH_CLI_UPDATES_CHECK=0") - } - e := &php.Executor{ - BinName: "php", - Args: append([]string{"php", p.path}, args...), - ExtraEnv: env, - } - e.Paths = append([]string{filepath.Dir(p.path)}, e.Paths...) - return e -} - -func (p *platformshCLI) RunInteractive(logger zerolog.Logger, projectDir string, args []string, debug bool, stdin io.Reader) (bytes.Buffer, bool) { - var buf bytes.Buffer - - e := p.executor(args) - if projectDir != "" { - e.Dir = projectDir - } - if debug { - e.Stdout = io.MultiWriter(&buf, os.Stdout) - e.Stderr = io.MultiWriter(&buf, os.Stderr) - } else { - e.Stdout = &buf - e.Stderr = &buf - } - if stdin != nil { - e.Stdin = stdin - } - logger.Debug().Str("cmd", strings.Join(e.Args, " ")).Msg("Executing Platform.sh CLI command interactively") - if ret := e.Execute(false); ret != 0 { - return buf, false - } - return buf, true -} - -func (p *platformshCLI) WrapHelpPrinter() func(w io.Writer, templ string, data interface{}) { - currentHelpPrinter := console.HelpPrinter - return func(w io.Writer, templ string, data interface{}) { - switch cmd := data.(type) { - case *console.Command: - if strings.HasPrefix(cmd.Category, "cloud") { - e := p.executor([]string{strings.TrimPrefix(cmd.FullName(), "cloud:"), "--help", "--ansi"}) - e.Execute(false) - } else { - currentHelpPrinter(w, templ, data) - } - default: - currentHelpPrinter(w, templ, data) - } - } -} diff --git a/commands/platformsh_hooks.go b/commands/platformsh_hooks.go index ea5d3ef9..b29641a3 100644 --- a/commands/platformsh_hooks.go +++ b/commands/platformsh_hooks.go @@ -1,6 +1,26 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + package commands import ( + "github.com/rs/zerolog" "github.com/symfony-cli/console" "github.com/symfony-cli/symfony-cli/envs" "github.com/symfony-cli/symfony-cli/local/platformsh" @@ -8,7 +28,15 @@ import ( ) var platformshBeforeHooks = map[string]console.BeforeFunc{ - "cloud:tunnel:close": func(c *console.Context) error { + "environment:push": func(c *console.Context) error { + // check that project has a DB and that server version is set properly + projectDir, err := getProjectDir(c.String("dir")) + if err != nil { + return err + } + return checkDoctrineServerVersionSetting(projectDir, zerolog.Nop()) + }, + "tunnel:close": func(c *console.Context) error { terminal.Eprintln("Stop exposing tunnel service environment variables") app := c.String("app") diff --git a/commands/platformsh_hooks_test.go b/commands/platformsh_hooks_test.go new file mode 100644 index 00000000..6d513ee9 --- /dev/null +++ b/commands/platformsh_hooks_test.go @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +import ( + "flag" + "strings" + "testing" + + "github.com/symfony-cli/console" +) + +func TestDeployHook(t *testing.T) { + flags := flag.NewFlagSet("test", 0) + flags.String("dir", "", "") + c := console.NewContext(nil, flags, nil) + + for dir, expected := range map[string]string{ + "testdata/platformsh/version-mismatch-env/": `The ".platform/services.yaml" file defines a "postgresql" version 14 database service but the ".env" file requires version 13.`, + "testdata/platformsh/version-mismatch-config/": `The ".platform/services.yaml" file defines a "postgresql" version 14 database service but the "config/packages/doctrine.yaml" file requires version 13.`, + "testdata/platformsh/ok/": ``, + "testdata/platformsh/mariadb-version/": ``, + "testdata/platformsh/missing-version/": `Set the "server_version" parameter to "14" in "config/packages/doctrine.yaml".`, + } { + flags.Set("dir", dir) + err := platformshBeforeHooks["environment:push"](c) + if err == nil { + if expected != "" { + t.Errorf("TestDeployHook(%q): got %v, expected %v", dir, err, expected) + } + continue + } + errString := strings.ReplaceAll(err.Error(), "\n", " ") + if expected == "" { + t.Errorf("TestDeployHook(%q): got %s, expected no errors", dir, errString) + } + if !strings.Contains(errString, expected) { + t.Errorf("TestDeployHook(%q): got %s, expected %s", dir, errString, expected) + } + } +} diff --git a/commands/project_init.go b/commands/project_init.go index 21ab6814..a8f474bc 100644 --- a/commands/project_init.go +++ b/commands/project_init.go @@ -28,6 +28,7 @@ import ( "github.com/symfony-cli/console" "github.com/symfony-cli/symfony-cli/git" + "github.com/symfony-cli/symfony-cli/local/platformsh" "github.com/symfony-cli/terminal" ) @@ -45,6 +46,7 @@ Templates used by this tool are fetched from ` + templatesGitRepository + `. &console.StringFlag{Name: "title", Usage: "Project title", DefaultText: "autodetermined based on directory name"}, &console.StringFlag{Name: "slug", DefaultValue: "app", Usage: "Project slug"}, &console.StringFlag{Name: "php", Usage: "PHP version to use"}, + &console.BoolFlag{Name: "upsun", Usage: "Initialize Upsun"}, // FIXME: services should also be used to configure Docker? Instead of Flex? // FIXME: services can also be guessed via the existing Docker Compose file? &console.StringSliceFlag{Name: "service", Usage: "Configure some services", Hidden: true}, @@ -79,7 +81,11 @@ Templates used by this tool are fetched from ` + templatesGitRepository + `. return err } - createdFiles, err := createRequiredFilesProject(projectDir, slug, c.String("template"), minorPHPVersion, cloudServices, c.Bool("dump"), c.Bool("force")) + brand := platformsh.PlatformshBrand + if c.Bool("upsun") { + brand = platformsh.UpsunBrand + } + createdFiles, err := createRequiredFilesProject(brand, projectDir, slug, c.String("template"), minorPHPVersion, cloudServices, c.Bool("dump"), c.Bool("force")) if err != nil { return err } @@ -101,7 +107,7 @@ Templates used by this tool are fetched from ` + templatesGitRepository + `. ui.Section("Next Steps") terminal.Println(" * Adapt the generated files if needed") - terminal.Printf(" * Commit them: git add %s && git commit -m\"Add Platform.sh configuration\"\n", strings.Join(createdFiles, " ")) + terminal.Printf(" * Commit them: git add %s && git commit -m\"Add %s configuration\"\n", strings.Join(createdFiles, " "), brand) terminal.Printf(" * Deploy: %s deploy\n", c.App.HelpName) } else { terminal.Printf("Deploy the project via %s deploy.\n", c.App.HelpName) @@ -115,5 +121,8 @@ func gitInit(cwd string) (*bytes.Buffer, error) { if _, err := os.Stat(filepath.Join(cwd, ".git")); err == nil || !os.IsNotExist(err) { return nil, nil } - return git.Init(cwd, false) + + // project:init is only used in a Cloud context, so we can safely force the + // branch to be "main" + return git.Init(cwd, true, false) } diff --git a/commands/resources/completion.bash b/commands/resources/completion.bash new file mode 100644 index 00000000..f6a3c601 --- /dev/null +++ b/commands/resources/completion.bash @@ -0,0 +1,92 @@ +# Copyright (c) 2021-present Fabien Potencier +# +# This file is part of Symfony CLI project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# Bash completions for the CLI binary +# +# References: +# - https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Console/Resources/completion.bash +# - https://github.com/posener/complete/blob/master/install/bash.go +# - https://github.com/scop/bash-completion/blob/master/completions/sudo +# + +_complete_{{ .App.HelpName }}() { + + # Use the default completion for shell redirect operators. + for w in '>' '>>' '&>' '<'; do + if [[ $w = "${COMP_WORDS[COMP_CWORD-1]}" ]]; then + compopt -o filenames + COMPREPLY=($(compgen -f -- "${COMP_WORDS[COMP_CWORD]}")) + return 0 + fi + done + + for (( i=1; i <= COMP_CWORD; i++ )); do + if [[ "${COMP_WORDS[i]}" != -* ]]; then + case "${COMP_WORDS[i]}" in + {{range $i, $name := (.App.Command "php").Names }}{{if $i}}|{{end}}{{$name}}{{end}}{{range $name := (.App.Command "run").Names }}|{{$name}}{{end}}) + _command_offset $i + return + ;; + esac; + fi + done + + # Use newline as only separator to allow space in completion values + local IFS=$'\n' + + local cur prev words cword + _get_comp_words_by_ref -n := cur prev words cword + + local sfcomplete + if sfcomplete=$(COMP_LINE="${COMP_LINE}" COMP_POINT="${COMP_POINT}" COMP_DEBUG="$COMP_DEBUG" CURRENT="$cword" {{ .CurrentBinaryPath }} self:autocomplete 2>&1); then + local quote suggestions + quote=${cur:0:1} + + # Use single quotes by default if suggestions contains backslash (FQCN) + if [ "$quote" == '' ] && [[ "$sfcomplete" =~ \\ ]]; then + quote=\' + fi + + if [ "$quote" == \' ]; then + # single quotes: no additional escaping (does not accept ' in values) + suggestions=$(for s in $sfcomplete; do printf $'%q%q%q\n' "$quote" "$s" "$quote"; done) + elif [ "$quote" == \" ]; then + # double quotes: double escaping for \ $ ` " + suggestions=$(for s in $sfcomplete; do + s=${s//\\/\\\\} + s=${s//\$/\\\$} + s=${s//\`/\\\`} + s=${s//\"/\\\"} + printf $'%q%q%q\n' "$quote" "$s" "$quote"; + done) + else + # no quotes: double escaping + suggestions=$(for s in $sfcomplete; do printf $'%q\n' $(printf '%q' "$s"); done) + fi + COMPREPLY=($(IFS=$'\n' compgen -W "$suggestions" -- $(printf -- "%q" "$cur"))) + __ltrim_colon_completions "$cur" + else + if [[ "$sfcomplete" != *"Command \"_complete\" is not defined."* ]]; then + >&2 echo + >&2 echo $sfcomplete + fi + + return 1 + fi +} + +complete -F _complete_{{ .App.HelpName }} {{ .App.HelpName }} diff --git a/commands/resources/completion.fish b/commands/resources/completion.fish new file mode 100644 index 00000000..81991327 --- /dev/null +++ b/commands/resources/completion.fish @@ -0,0 +1,36 @@ +# Copyright (c) 2021-present Fabien Potencier +# +# This file is part of Symfony CLI project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# Fish completions for the CLI binary +# +# References: +# - https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Console/Resources/completion.fish +# - https://github.com/posener/complete/blob/master/install/fish.go +# - https://github.com/fish-shell/fish-shell/blob/master/share/completions/sudo.fish +# + +function __complete_{{ .App.HelpName }} + set -lx COMP_LINE (commandline -cp) + test -z (commandline -ct) + and set COMP_LINE "$COMP_LINE " + set -x CURRENT (count (commandline -oc)) + {{ .CurrentBinaryInvocation }} self:autocomplete +end + +complete -f -c '{{ .App.HelpName }}' -n "__fish_seen_subcommand_from {{range $i, $name := (.App.Command "php").Names }}{{if $i}} {{end}}{{$name}}{{end}}" -a '(__fish_complete_subcommand)' +complete -f -c '{{ .App.HelpName }}' -n "__fish_seen_subcommand_from {{range $i, $name := (.App.Command "run").Names }}{{if $i}} {{end}}{{$name}}{{end}}" -a '(__fish_complete_subcommand --fcs-skip=2)' +complete -f -c '{{ .App.HelpName }}' -n "not __fish_seen_subcommand_from {{range $i, $name := (.App.Command "php").Names }}{{if $i}} {{end}}{{$name}}{{end}} {{range $i, $name := (.App.Command "run").Names }}{{if $i}} {{end}}{{$name}}{{end}}" -a '(__complete_{{ .App.HelpName }})' diff --git a/commands/resources/completion.zsh b/commands/resources/completion.zsh new file mode 100644 index 00000000..77c731db --- /dev/null +++ b/commands/resources/completion.zsh @@ -0,0 +1,89 @@ +#compdef {{ .App.HelpName }} + +# Copyright (c) 2021-present Fabien Potencier +# +# This file is part of Symfony CLI project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# +# zsh completions for {{ .App.HelpName }} +# +# References: +# - https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Console/Resources/completion.zsh +# - https://github.com/posener/complete/blob/master/install/zsh.go +# - https://stackoverflow.com/a/13547531 +# + +_complete_{{ .App.HelpName }}() { + local lastParam flagPrefix requestComp out comp + local -a completions + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") lastParam=${words[-1]} + + # For zsh, when completing a flag with an = (e.g., {{ .App.HelpName }} -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # detect if we are in a wrapper command and need to "forward" completion to it + for ((i = 1; i <= $#words; i++)); do + if [[ "${words[i]}" != -* ]]; then + case "${words[i]}" in + console|composer) + (( CURRENT-- )) + ;; + {{range $i, $name := (.App.Command "php").Names }}{{if $i}}|{{end}}{{$name}}{{end}}) + shift words + (( CURRENT-- )) + _normal + return + ;; + {{range $i, $name := (.App.Command "local:run").Names }}{{if $i}}|{{end}}{{$name}}{{end}}) + shift words + (( CURRENT-- )) + shift words + (( CURRENT-- )) + _normal + return + ;; + esac; + fi + done + + while IFS='\n' read -r comp; do + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + local tab=$(printf '\t') + comp=${comp//$tab/:} + completions+=${comp} + fi + done < <(COMP_LINE="$words" CURRENT="$CURRENT" ${words[0]} ${_SF_CMD:-${words[1]}} self:autocomplete) + + # Let inbuilt _describe handle completions + eval _describe "completions" completions $flagPrefix +} + +compdef _complete_{{ .App.HelpName }} {{ .App.HelpName }} diff --git a/commands/root.go b/commands/root.go index 25c00a6d..606191ef 100644 --- a/commands/root.go +++ b/commands/root.go @@ -20,14 +20,13 @@ package commands import ( - "os" "os/exec" "path/filepath" - "sync" "github.com/pkg/errors" - "github.com/spf13/viper" "github.com/symfony-cli/console" + "github.com/symfony-cli/symfony-cli/envs" + "github.com/symfony-cli/symfony-cli/local/platformsh" "github.com/symfony-cli/symfony-cli/reexec" "github.com/symfony-cli/symfony-cli/updater" "github.com/symfony-cli/symfony-cli/util" @@ -35,9 +34,6 @@ import ( ) var ( - psh *platformshCLI - pshOnce sync.Once - dirFlag = &console.StringFlag{Name: "dir", Usage: "Project directory"} projectFlag = &console.StringFlag{Name: "project", Aliases: []string{"p"}, Usage: "The project ID or URL"} environmentFlag = &console.StringFlag{Name: "environment", Aliases: []string{"e"}, Usage: "The environment ID"} @@ -60,13 +56,16 @@ func CommonCommands() []*console.Command { bookCheckReqsCmd, bookCheckoutCmd, cloudEnvDebugCmd, + doctrineCheckServerVersionSettingCmd, localNewCmd, localPhpListCmd, localPhpRefreshCmd, + localProxyTLD, localProxyAttachDomainCmd, localProxyDetachDomainCmd, localProxyStartCmd, localProxyStatusCmd, + localProxyUrlCmd, localProxyStopCmd, localRequirementsCheckCmd, localRunCmd, @@ -82,6 +81,7 @@ func CommonCommands() []*console.Command { localSecurityCheckCmd, projectLocalMailCatcherOpenCmd, projectLocalRabbitMQManagementOpenCmd, + projectLocalServiceOpenCmd, projectLocalOpenCmd, } return append(localCommands, adminCommands...) @@ -97,36 +97,37 @@ func CheckGitIsAvailable(c *console.Context) error { func init() { initCLI() - initConfig() -} - -func GetPSH() (*platformshCLI, error) { - var err error - pshOnce.Do(func() { - psh, err = NewPlatformShCLI() - if err != nil { - err = errors.Wrap(err, "Unable to setup Platform.sh CLI") - } - }) - return psh, err } func InitAppFunc(c *console.Context) error { - if c.App.Channel == "stable" { - // do not run auto-update in the cloud, CI or background jobs - if !util.InCloud() && terminal.Stdin.IsInteractive() && !reexec.IsChild() { - debug := false - if os.Getenv("SC_DEBUG") == "1" { - debug = true - } - updater := updater.NewUpdater(filepath.Join(util.GetHomeDir(), "update"), c.App.ErrWriter, debug) - updater.CheckForNewVersion(c.App.Version) - } + checkWSL() + + envs.ComputeDockerUserAgent(c.App.Name, c.App.Version) + + psh, err := platformsh.Get() + if err != nil { + return err + } + for name, f := range platformshBeforeHooks { + psh.AddBeforeHook(name, f) } + checkForUpdates(c) return nil } +func checkForUpdates(c *console.Context) { + if c.App.Channel != "stable" { + return + } + // do not run auto-update in the cloud, CI or background jobs + if util.InCloud() || !terminal.Stdin.IsInteractive() || reexec.IsChild() { + return + } + updater := updater.NewUpdater(filepath.Join(util.GetHomeDir(), "update"), c.App.ErrWriter, terminal.IsDebug()) + updater.CheckForNewVersion(c.App.Version) +} + // WelcomeAction displays a message when no command func WelcomeAction(c *console.Context) error { console.ShowVersion(c) @@ -148,7 +149,7 @@ func WelcomeAction(c *console.Context) error { terminal.Println("") terminal.Println("Manage a project on Cloud") terminal.Println("") - psh, err := GetPSH() + psh, err := platformsh.Get() if err != nil { return err } @@ -173,32 +174,17 @@ func initCLI() { console.AppHelpTemplate += ` Available wrappers: Runs PHP (version depends on project's configuration). -Environment variables to use Platform.sh relationships or Docker services are automatically defined. +Environment variables to use Platform.sh/Upsun relationships or Docker services are automatically defined. {{with .Command "composer"}} {{.PreferredName}}{{"\t"}}{{.Usage}}{{end}} {{with .Command "console"}} {{.PreferredName}}{{"\t"}}{{.Usage}}{{end}} {{with .Command "php"}} {{.PreferredName}}{{"\t"}}{{.Usage}}{{end}} +{{with .Command "pie"}} {{.PreferredName}}{{"\t"}}{{.Usage}}{{end}} ` } -// initConfig reads in config file and ENV variables if set. -func initConfig() { - if os.Getenv("SF_CONFIG") != "" { - viper.SetConfigFile(os.Getenv("SF_CONFIG")) - } - viper.SetConfigName("symfony") - viper.AddConfigPath("$HOME/.symfony") - viper.AddConfigPath(".") - viper.AutomaticEnv() - viper.SetEnvPrefix("SYMFONY") - if err := viper.ReadInConfig(); err == nil { - terminal.Logger.Info().Msg("Using config file: " + viper.ConfigFileUsed()) - } -} - func getProjectDir(dir string) (string, error) { - var err error if dir, err = filepath.Abs(dir); err != nil { return "", err diff --git a/commands/tablewriter_patch_ansi.go b/commands/tablewriter_patch_ansi.go new file mode 100644 index 00000000..04ce10ce --- /dev/null +++ b/commands/tablewriter_patch_ansi.go @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +import ( + "regexp" + _ "unsafe" +) + +// Temporary workaround allowing github.com/olekukonko/tablewriter to properly +// format tables with rows containing ANSI terminal links. A PR +// (https://github.com/olekukonko/tablewriter/pull/206) has been opened +// upstream, but we don't know if and when it will be merged. This Go +// compilation directive allows to access the unexported variable and update it +// with what we submitted upstream. +// To be removed once the PR is merged and released. +// +//go:linkname tableAnsiEscapingRegexp github.com/olekukonko/tablewriter.ansi +var tableAnsiEscapingRegexp = regexp.MustCompile("\033(?:\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]|\\]8;;.*?\033\\\\)") diff --git a/commands/testdata/docker/postgresql/10/docker-compose.yml b/commands/testdata/docker/postgresql/10/docker-compose.yml new file mode 100644 index 00000000..aef31ffe --- /dev/null +++ b/commands/testdata/docker/postgresql/10/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3' + +services: + database: + image: postgres:10-alpine + ports: + - "5432" diff --git a/commands/testdata/docker/postgresql/next/docker-compose.yml b/commands/testdata/docker/postgresql/next/docker-compose.yml new file mode 100644 index 00000000..3653de2d --- /dev/null +++ b/commands/testdata/docker/postgresql/next/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3' + +services: + database: + image: postgres:${POSTGRES_NEXT_VERSION}-alpine + ports: + - "5432" diff --git a/commands/testdata/docker/postgresql/noversion/docker-compose.yml b/commands/testdata/docker/postgresql/noversion/docker-compose.yml new file mode 100644 index 00000000..939c77c2 --- /dev/null +++ b/commands/testdata/docker/postgresql/noversion/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3' + +services: + database: + image: postgres + ports: + - "5432" diff --git a/commands/testdata/platformsh/mariadb-version/.env b/commands/testdata/platformsh/mariadb-version/.env new file mode 100644 index 00000000..0cc8154b --- /dev/null +++ b/commands/testdata/platformsh/mariadb-version/.env @@ -0,0 +1,8 @@ +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" +DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=mariadb-10.6.12&charset=utf8" +###< doctrine/doctrine-bundle ### diff --git a/commands/testdata/platformsh/mariadb-version/.platform.app.yaml b/commands/testdata/platformsh/mariadb-version/.platform.app.yaml new file mode 100644 index 00000000..06798c32 --- /dev/null +++ b/commands/testdata/platformsh/mariadb-version/.platform.app.yaml @@ -0,0 +1,27 @@ +name: app + +type: php:8.2 + +relationships: + database: "mysqldb:mysql" + +web: + locations: + "/": + root: "public" + expires: 1d + passthru: "/index.php" + +disk: 8192 + +hooks: + build: | + set -x -e + + curl -s https://get.symfony.com/cloud/configurator | bash + symfony-build + + deploy: | + set -x -e + + symfony-deploy diff --git a/commands/testdata/platformsh/mariadb-version/.platform/services.yaml b/commands/testdata/platformsh/mariadb-version/.platform/services.yaml new file mode 100644 index 00000000..d7c1672c --- /dev/null +++ b/commands/testdata/platformsh/mariadb-version/.platform/services.yaml @@ -0,0 +1,3 @@ +mysqldb: + type: mysql:10.6 + disk: 512 diff --git a/commands/testdata/platformsh/missing-version/.env b/commands/testdata/platformsh/missing-version/.env new file mode 100644 index 00000000..127cdaa1 --- /dev/null +++ b/commands/testdata/platformsh/missing-version/.env @@ -0,0 +1,8 @@ +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" +DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?charset=utf8" +###< doctrine/doctrine-bundle ### diff --git a/commands/testdata/platformsh/missing-version/.platform/services.yaml b/commands/testdata/platformsh/missing-version/.platform/services.yaml new file mode 100644 index 00000000..376f6a70 --- /dev/null +++ b/commands/testdata/platformsh/missing-version/.platform/services.yaml @@ -0,0 +1,3 @@ +pgsqldb: + type: postgresql:14 + disk: 512 diff --git a/commands/testdata/platformsh/ok/.env b/commands/testdata/platformsh/ok/.env new file mode 100644 index 00000000..1dc0a35e --- /dev/null +++ b/commands/testdata/platformsh/ok/.env @@ -0,0 +1,8 @@ +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" +DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=14&charset=utf8" +###< doctrine/doctrine-bundle ### diff --git a/commands/testdata/platformsh/ok/.platform.app.yaml b/commands/testdata/platformsh/ok/.platform.app.yaml new file mode 100644 index 00000000..42aa30c9 --- /dev/null +++ b/commands/testdata/platformsh/ok/.platform.app.yaml @@ -0,0 +1,27 @@ +name: app + +type: php:8.2 + +relationships: + database: "pgsqldb:postgresql" + +web: + locations: + "/": + root: "public" + expires: 1d + passthru: "/index.php" + +disk: 8192 + +hooks: + build: | + set -x -e + + curl -s https://get.symfony.com/cloud/configurator | bash + symfony-build + + deploy: | + set -x -e + + symfony-deploy diff --git a/commands/testdata/platformsh/ok/.platform/services.yaml b/commands/testdata/platformsh/ok/.platform/services.yaml new file mode 100644 index 00000000..376f6a70 --- /dev/null +++ b/commands/testdata/platformsh/ok/.platform/services.yaml @@ -0,0 +1,3 @@ +pgsqldb: + type: postgresql:14 + disk: 512 diff --git a/commands/testdata/platformsh/ok/config/packages/doctrine.yaml b/commands/testdata/platformsh/ok/config/packages/doctrine.yaml new file mode 100644 index 00000000..36b818e2 --- /dev/null +++ b/commands/testdata/platformsh/ok/config/packages/doctrine.yaml @@ -0,0 +1,5 @@ +doctrine: + dbal: + driver: pdo_pgsql + server_version: '14' + url: '%env(resolve:DATABASE_URL)%' diff --git a/commands/testdata/platformsh/version-mismatch-config/.platform.app.yaml b/commands/testdata/platformsh/version-mismatch-config/.platform.app.yaml new file mode 100644 index 00000000..42aa30c9 --- /dev/null +++ b/commands/testdata/platformsh/version-mismatch-config/.platform.app.yaml @@ -0,0 +1,27 @@ +name: app + +type: php:8.2 + +relationships: + database: "pgsqldb:postgresql" + +web: + locations: + "/": + root: "public" + expires: 1d + passthru: "/index.php" + +disk: 8192 + +hooks: + build: | + set -x -e + + curl -s https://get.symfony.com/cloud/configurator | bash + symfony-build + + deploy: | + set -x -e + + symfony-deploy diff --git a/commands/testdata/platformsh/version-mismatch-config/.platform/services.yaml b/commands/testdata/platformsh/version-mismatch-config/.platform/services.yaml new file mode 100644 index 00000000..376f6a70 --- /dev/null +++ b/commands/testdata/platformsh/version-mismatch-config/.platform/services.yaml @@ -0,0 +1,3 @@ +pgsqldb: + type: postgresql:14 + disk: 512 diff --git a/commands/testdata/platformsh/version-mismatch-config/config/packages/doctrine.yaml b/commands/testdata/platformsh/version-mismatch-config/config/packages/doctrine.yaml new file mode 100644 index 00000000..49278b94 --- /dev/null +++ b/commands/testdata/platformsh/version-mismatch-config/config/packages/doctrine.yaml @@ -0,0 +1,5 @@ +doctrine: + dbal: + driver: pdo_pgsql + server_version: '13' + url: '%env(resolve:DATABASE_URL)%' diff --git a/commands/testdata/platformsh/version-mismatch-env/.env b/commands/testdata/platformsh/version-mismatch-env/.env new file mode 100644 index 00000000..bec9b2e0 --- /dev/null +++ b/commands/testdata/platformsh/version-mismatch-env/.env @@ -0,0 +1,8 @@ +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" +DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?charset=utf8&serverVersion=13" +###< doctrine/doctrine-bundle ### diff --git a/commands/testdata/platformsh/version-mismatch-env/.platform.app.yaml b/commands/testdata/platformsh/version-mismatch-env/.platform.app.yaml new file mode 100644 index 00000000..42aa30c9 --- /dev/null +++ b/commands/testdata/platformsh/version-mismatch-env/.platform.app.yaml @@ -0,0 +1,27 @@ +name: app + +type: php:8.2 + +relationships: + database: "pgsqldb:postgresql" + +web: + locations: + "/": + root: "public" + expires: 1d + passthru: "/index.php" + +disk: 8192 + +hooks: + build: | + set -x -e + + curl -s https://get.symfony.com/cloud/configurator | bash + symfony-build + + deploy: | + set -x -e + + symfony-deploy diff --git a/commands/testdata/platformsh/version-mismatch-env/.platform/services.yaml b/commands/testdata/platformsh/version-mismatch-env/.platform/services.yaml new file mode 100644 index 00000000..376f6a70 --- /dev/null +++ b/commands/testdata/platformsh/version-mismatch-env/.platform/services.yaml @@ -0,0 +1,3 @@ +pgsqldb: + type: postgresql:14 + disk: 512 diff --git a/commands/testdata/project/.platform.app.yaml b/commands/testdata/project/.platform.app.yaml new file mode 100644 index 00000000..84e0accc --- /dev/null +++ b/commands/testdata/project/.platform.app.yaml @@ -0,0 +1,67 @@ +name: slug + +type: php:8.0 + +dependencies: + php: + composer/composer: "^2" + +runtime: + extensions: + - apcu + - blackfire + - ctype + - iconv + - mbstring + - pdo_pgsql + - sodium + - xsl + + +variables: + php: + opcache.preload: config/preload.php +build: + flavor: none + +disk: 1024 + +web: + locations: + "/": + root: "public" + expires: 1h + passthru: "/index.php" + +mounts: + "/var/cache": { source: local, source_path: var/cache } + + +relationships: + foo: "foo:bar" + foo1: "foo1:bar1" + foo2: "foo2:postgresql" + +hooks: + build: | + set -x -e + + curl -fs https://get.symfony.com/cloud/configurator | bash + + NODE_VERSION=22 symfony-build + + deploy: | + set -x -e + + symfony-deploy + +crons: + security-check: + # Check that no security issues have been found for PHP packages deployed in production + spec: '50 23 * * *' + cmd: if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then croncape COMPOSER_ROOT_VERSION=1.0.0 COMPOSER_AUDIT_ABANDONED=ignore composer audit --no-cache; fi + clean-expired-sessions: + spec: '17,47 * * * *' + cmd: croncape php-session-clean + + diff --git a/commands/testdata/project/.platform/routes.yaml b/commands/testdata/project/.platform/routes.yaml new file mode 100644 index 00000000..3c9483b6 --- /dev/null +++ b/commands/testdata/project/.platform/routes.yaml @@ -0,0 +1,2 @@ +"https://{all}/": { type: upstream, upstream: "slug:http" } +"http://{all}/": { type: redirect, to: "https://{all}/" } diff --git a/commands/testdata/project/.platform/services.yaml b/commands/testdata/project/.platform/services.yaml new file mode 100644 index 00000000..b2ba9ff3 --- /dev/null +++ b/commands/testdata/project/.platform/services.yaml @@ -0,0 +1,12 @@ + +foo: + type: bar:baz + +foo1: + type: bar1:baz1 + +foo2: + type: postgresql:baz2 + disk: 1024 + + diff --git a/commands/testdata/project/.upsun/config.yaml b/commands/testdata/project/.upsun/config.yaml new file mode 100644 index 00000000..c1e767bc --- /dev/null +++ b/commands/testdata/project/.upsun/config.yaml @@ -0,0 +1,80 @@ +routes: + "https://{all}/": { type: upstream, upstream: "slug:http" } + "http://{all}/": { type: redirect, to: "https://{all}/" } + +services: + foo: + type: bar:baz + + foo1: + type: bar1:baz1 + + foo2: + type: postgresql:baz2 + + + +applications: + slug: + source: + root: "/" + + type: php:8.0 + + runtime: + extensions: + - apcu + - blackfire + - ctype + - iconv + - mbstring + - pdo_pgsql + - sodium + - xsl + + + variables: + php: + opcache.preload: config/preload.php + build: + flavor: none + + web: + locations: + "/": + root: "public" + expires: 1h + passthru: "/index.php" + + mounts: + "/var": { source: storage, source_path: var } + + + relationships: + foo: "foo:bar" + foo1: "foo1:bar1" + foo2: "foo2:postgresql" + + hooks: + build: | + set -x -e + + curl -fs https://get.symfony.com/cloud/configurator | bash + + NODE_VERSION=22 symfony-build + + deploy: | + set -x -e + + symfony-deploy + + crons: + security-check: + # Check that no security issues have been found for PHP packages deployed in production + spec: '50 23 * * *' + cmd: if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then croncape COMPOSER_ROOT_VERSION=1.0.0 COMPOSER_AUDIT_ABANDONED=ignore composer audit --no-cache; fi + clean-expired-sessions: + spec: '17,47 * * * *' + cmd: croncape php-session-clean + + diff --git a/commands/testdata/project/composer.json b/commands/testdata/project/composer.json new file mode 100644 index 00000000..e69de29b diff --git a/commands/testdata/project/composer.lock b/commands/testdata/project/composer.lock new file mode 100644 index 00000000..495cd0d3 --- /dev/null +++ b/commands/testdata/project/composer.lock @@ -0,0 +1,120 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "f369468988f7ddaabf0a8c690d4307df", + "packages": [ + { + "name": "symfony/flex", + "version": "v2.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1", + "reference": "6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.1", + "php": ">=8.0" + }, + "require-dev": { + "composer/composer": "^2.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Flex\\Flex" + }, + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "Composer plugin for Symfony", + "support": { + "issues": "https://github.com/symfony/flex/issues", + "source": "https://github.com/symfony/flex/tree/v2.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-02T11:08:32+00:00" + }, + { + "name": "symfonycorp/platformsh-meta", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfonycorp/platformsh-meta.git", + "reference": "3e8559c75ed759921015cc7ebf5952c306afcaf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfonycorp/platformsh-meta/zipball/3e8559c75ed759921015cc7ebf5952c306afcaf8", + "reference": "3e8559c75ed759921015cc7ebf5952c306afcaf8", + "shasum": "" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A meta package for platformsh projects", + "support": { + "issues": "https://github.com/symfonycorp/platformsh-meta/issues", + "source": "https://github.com/symfonycorp/platformsh-meta/tree/v1.0.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/symfonycorp/connect", + "type": "tidelift" + } + ], + "time": "2021-10-20T16:38:02+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.2", + "ext-ctype": "*", + "ext-iconv": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/commands/testdata/project/php.ini b/commands/testdata/project/php.ini new file mode 100644 index 00000000..6efbd07c --- /dev/null +++ b/commands/testdata/project/php.ini @@ -0,0 +1,7 @@ +allow_url_include=off +display_errors=off +display_startup_errors=off +max_execution_time=30 +session.use_strict_mode=On +realpath_cache_ttl=3600 +zend.detect_unicode=Off diff --git a/commands/wrappers.go b/commands/wrappers.go index ae6a4fd1..1326524b 100644 --- a/commands/wrappers.go +++ b/commands/wrappers.go @@ -27,32 +27,46 @@ import ( var ( composerWrapper = &console.Command{ - Name: "composer", Usage: "Runs Composer without memory limit", Hidden: console.Hide, + // we use an alias to avoid the command being shown in the help but + // still be available for completion + Aliases: []*console.Alias{{Name: "composer"}}, + ShellComplete: autocompleteComposerWrapper, Action: func(c *console.Context) error { return console.IncorrectUsageError{ParentError: errors.New(`This command can only be run as "symfony composer"`)} }, } binConsoleWrapper = &console.Command{ - Name: "console", Usage: "Runs the Symfony Console (bin/console) for current project", Hidden: console.Hide, + // we use an alias to avoid the command being shown in the help but + // still be available for completion + Aliases: []*console.Alias{{Name: "console"}}, Action: func(c *console.Context) error { - return console.IncorrectUsageError{ParentError: errors.New(`This command can only be run as "symfony console"`)} + return errors.New(`No Symfony console detected to run "symfony console"`) }, + ShellComplete: autocompleteSymfonyConsoleWrapper, } phpWrapper = &console.Command{ Usage: "Runs the named binary using the configured PHP version", Hidden: console.Hide, + // we use aliases to avoid the command being shown in the help but + // still be available for completion + Aliases: func() []*console.Alias { + binNames := php.GetBinaryNames() + aliases := make([]*console.Alias, 0, len(binNames)+1) + + for _, name := range php.GetBinaryNames() { + aliases = append(aliases, &console.Alias{Name: name}) + } + + aliases = append(aliases, &console.Alias{Name: "pie"}) + + return aliases + }(), Action: func(c *console.Context) error { return console.IncorrectUsageError{ParentError: errors.New(`This command can only be run as "symfony php*"`)} }, } ) - -func init() { - for _, name := range php.GetBinaryNames() { - phpWrapper.Aliases = append(phpWrapper.Aliases, &console.Alias{Name: name, Hidden: console.Hide()}) - } -} diff --git a/commands/wsl_others.go b/commands/wsl_others.go new file mode 100644 index 00000000..9ddae2ed --- /dev/null +++ b/commands/wsl_others.go @@ -0,0 +1,26 @@ +//go:build !windows +// +build !windows + +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +func checkWSL() { +} diff --git a/commands/wsl_windows.go b/commands/wsl_windows.go new file mode 100644 index 00000000..10301a4b --- /dev/null +++ b/commands/wsl_windows.go @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package commands + +import ( + "os" + + "github.com/symfony-cli/terminal" +) + +func checkWSL() { + if fi, err := os.Stat("/proc/version"); fi == nil || err != nil { + return + } + + ui := terminal.SymfonyStyle(terminal.Stdout, terminal.Stdin) + ui.Error("Wrong binary for WSL") + terminal.Println(`You are trying to run the Windows version of the Symfony CLI on WSL (Linux). +You must use the Linux version to use the Symfony CLI on WSL. + +Download it at https://symfony.com/download +`) + os.Exit(1) +} diff --git a/envs/docker.go b/envs/docker.go index 94048ed5..7efe2cb2 100644 --- a/envs/docker.go +++ b/envs/docker.go @@ -19,12 +19,12 @@ package envs +//go:generate sh generate_docker_version + import ( "bytes" "context" "fmt" - "io/ioutil" - "net" "net/url" "os" "path/filepath" @@ -34,17 +34,25 @@ import ( "strings" "time" - "github.com/docker/docker/api/types" + compose "github.com/compose-spec/compose-go/cli" + composeConsts "github.com/compose-spec/compose-go/consts" + "github.com/docker/docker/api/types/container" docker "github.com/docker/docker/client" "github.com/symfony-cli/terminal" + "gopkg.in/yaml.v2" ) var ( dockerComposeNormalizeRegexp = regexp.MustCompile("[^-_a-z0-9]") dockerComposeNormalizeRegexpLegacy = regexp.MustCompile("[^a-z0-9]") + dockerUserAgent = "Docker-Client/unknown version" ) -type sortedPorts []types.Port +func ComputeDockerUserAgent(appName, appVersion string) { + dockerUserAgent = fmt.Sprintf("Docker-Client/%s %s/%s", dockerClientVersion, appName, appVersion) +} + +type sortedPorts []container.Port func (ps sortedPorts) Len() int { return len(ps) } func (ps sortedPorts) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] } @@ -58,7 +66,7 @@ func normalizeDockerComposeProjectName(projectName string) string { // Port of https://github.com/docker/compose/blob/4e0fdd70bdae4f8d85e4ef9d0129ce445f3ece3c/compose/cli/command.py#L129-L130 // (prior to 615c01c50a51408a7fdfed66ecccf73781e87f2c) // This was used in Docker Compose prior to 1.21.0, some users might still use -// versions older though, so we keep this BC in the mean time. +// versions older though, so we keep this BC in the meantime. func normalizeDockerComposeProjectNameLegacy(projectName string) string { return dockerComposeNormalizeRegexpLegacy.ReplaceAllString(strings.ToLower(projectName), "") } @@ -69,17 +77,17 @@ func (l *Local) RelationshipsFromDocker() Relationships { return nil } - opts := [](docker.Opt){docker.FromEnv} - if host := os.Getenv("DOCKER_HOST"); host != "" && !strings.HasPrefix(host, "unix://") { - // Setting a dialer on top of a unix socket breaks the connection - // as the client then tries to connect to http:///path/to/socket and - // thus tries to resolve the /path/to/socket host - dialer := &net.Dialer{ - Timeout: 2 * time.Second, - } - opts = append(opts, docker.WithDialer(dialer)) - } - client, err := docker.NewClientWithOpts(opts...) + client, err := docker.NewClientWithOpts( + docker.FromEnv, + dockerUseDesktopSocketIfAvailable, + docker.WithAPIVersionNegotiation(), + // we use a short timeout here because we don't want to impact + // negatively performance when Docker is not reachable + docker.WithTimeout(2*time.Second), + // defining a User Agent to avoid having the Docker API being slow + // see https://github.com/docker/for-mac/issues/7575 + docker.WithUserAgent(dockerUserAgent), + ) if err != nil { if l.Debug { fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) @@ -88,13 +96,12 @@ func (l *Local) RelationshipsFromDocker() Relationships { } defer client.Close() - client.NegotiateAPIVersion(context.Background()) - - containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{}) + containers, err := client.ContainerList(context.Background(), container.ListOptions{}) if err != nil { if docker.IsErrConnectionFailed(err) { terminal.Logger.Warn().Msg(err.Error()) - } else if l.Debug { + } + if l.Debug { fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) } return nil @@ -140,7 +147,7 @@ func (l *Local) RelationshipsFromDocker() Relationships { return relationships } -func (l *Local) dockerServiceToRelationship(client *docker.Client, container types.Container) map[string]map[string]interface{} { +func (l *Local) dockerServiceToRelationship(client *docker.Client, container container.Summary) map[string]map[string]interface{} { if l.Debug { fmt.Fprintf(os.Stderr, `found Docker container "%s" for project "%s" (image "%s")`+"\n", container.Labels["com.docker.compose.service"], container.Labels["com.docker.compose.project"], container.Image) } @@ -183,261 +190,317 @@ func (l *Local) dockerServiceToRelationship(client *docker.Client, container typ } } - host := os.Getenv("DOCKER_HOST") + host := os.Getenv(docker.EnvOverrideHost) if host == "" || strings.HasPrefix(host, "unix://") { host = "127.0.0.1" } else { u, err := url.Parse(host) if err != nil { - fmt.Fprintf(os.Stderr, " ERROR: unable to parse DOCKER_HOST \"%s\", falling back to 127.0.0.1: %s\n", host, err) + fmt.Fprintf(os.Stderr, " ERROR: unable to parse %s \"%s\", falling back to 127.0.0.1: %s\n", docker.EnvOverrideHost, host, err) host = "127.0.0.1" } else { host = u.Hostname() } } + if len(exposedPorts) < 1 { + return nil + } + sort.Sort(exposedPorts) - for _, p := range exposedPorts { - rels := make(map[string]map[string]interface{}) - if p.PrivatePort == 1025 { - // recommended image: schickling/mailcatcher - for _, pw := range exposedPorts { - if pw.PrivatePort == 1080 || pw.PrivatePort == 8025 { - rels["-web"] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(pw.PublicPort), - "rel": "mailer", - "scheme": "http", - } - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(p.PublicPort), - "rel": "mailer", - "scheme": "smtp", - } - return rels + p := exposedPorts[0] + + rels := make(map[string]map[string]interface{}) + if p.PrivatePort == 1025 { + // recommended image: sj26/mailcatcher or axllent/mailpit (default now) + for _, pw := range exposedPorts { + if pw.PrivatePort == 1080 || pw.PrivatePort == 8025 { + rels["-web"] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(pw.PublicPort), + "rel": "mailer", + "scheme": "http", } - } - } else if p.PrivatePort == 25 { - // recommended image: djfarrelly/maildev - for _, pw := range exposedPorts { - if pw.PrivatePort == 80 { - rels["-web"] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(pw.PublicPort), - "rel": "mailer", - "scheme": "http", - } - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(p.PublicPort), - "rel": "mailer", - "scheme": "smtp", - } - return rels + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(p.PublicPort), + "rel": "mailer", + "scheme": "smtp", } + return rels } - } else if p.PrivatePort == 8707 || p.PrivatePort == 8307 { - // Blackfire - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(p.PublicPort), - "rel": "blackfire", - "scheme": "tcp", - } - return rels - } else if p.PrivatePort == 3306 { - username := "" - password := "" - path := "" - // MARIADB is used by bitnami/mariadb - for _, prefix := range []string{"MYSQL", "MARIADB"} { - for _, env := range c.Config.Env { - if strings.HasPrefix(env, prefix+"_ROOT_PASSWORD") && password == "" { - // *_PASSWORD has precedence over *_ROOT_PASSWORD - password = getEnvValue(env, prefix+"_ROOT_PASSWORD") - username = "root" - } else if strings.HasPrefix(env, prefix+"_USER") { - username = getEnvValue(env, prefix+"_USER") - } else if strings.HasPrefix(env, prefix+"_PASSWORD") { - password = getEnvValue(env, prefix+"_PASSWORD") - } else if strings.HasPrefix(env, prefix+"_DATABASE") { - path = getEnvValue(env, prefix+"_DATABASE") - } + } + } else if p.PrivatePort == 25 { + // recommended image: djfarrelly/maildev + for _, pw := range exposedPorts { + if pw.PrivatePort == 80 { + rels["-web"] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(pw.PublicPort), + "rel": "mailer", + "scheme": "http", } - } - if path == "" { - path = username - } - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "username": username, - "password": password, - "path": path, - "port": formatDockerPort(p.PublicPort), - "query": map[string]bool{ - "is_master": true, - }, - "rel": "mysql", - "scheme": "mysql", - } - return rels - } else if p.PrivatePort == 5432 { - username := "" - password := "" - path := "" - for _, env := range c.Config.Env { - if strings.HasPrefix(env, "POSTGRES_USER") { - username = getEnvValue(env, "POSTGRES_USER") - } else if strings.HasPrefix(env, "POSTGRES_PASSWORD") { - password = getEnvValue(env, "POSTGRES_PASSWORD") - } else if strings.HasPrefix(env, "POSTGRES_DB") { - path = getEnvValue(env, "POSTGRES_DB") + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(p.PublicPort), + "rel": "mailer", + "scheme": "smtp", } + return rels } - if path == "" { - path = username - } - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "username": username, - "password": password, - "path": path, - "port": formatDockerPort(p.PublicPort), - "query": map[string]bool{ - "is_master": true, - }, - "rel": "pgsql", - "scheme": "pgsql", - } - return rels - } else if p.PrivatePort == 6379 { - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(p.PublicPort), - "rel": "redis", - "scheme": "redis", - } - return rels - } else if p.PrivatePort == 11211 { - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(p.PublicPort), - "rel": "memcached", - "scheme": "memcached", - } - return rels - } else if p.PrivatePort == 5672 { - username := "guest" - password := "guest" + } + } else if p.PrivatePort == 8707 || p.PrivatePort == 8307 { + // Blackfire + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(p.PublicPort), + "rel": "blackfire", + "scheme": "tcp", + } + return rels + } else if p.PrivatePort == 3306 { + username := "" + password := "" + path := "" + version := "" + // MARIADB is used by bitnami/mariadb + for _, prefix := range []string{"MYSQL", "MARIADB"} { for _, env := range c.Config.Env { - // that's our local convention - if strings.HasPrefix(env, "RABBITMQ_DEFAULT_USER") { - username = getEnvValue(env, "RABBITMQ_DEFAULT_USER") - } else if strings.HasPrefix(env, "RABBITMQ_DEFAULT_PASS") { - password = getEnvValue(env, "RABBITMQ_DEFAULT_PASS") + if strings.HasPrefix(env, prefix+"_ROOT_PASSWORD") && password == "" { + // *_PASSWORD has precedence over *_ROOT_PASSWORD + password = getEnvValue(env, prefix+"_ROOT_PASSWORD") + username = "root" + } else if strings.HasPrefix(env, prefix+"_USER") { + username = getEnvValue(env, prefix+"_USER") + } else if strings.HasPrefix(env, prefix+"_PASSWORD") { + password = getEnvValue(env, prefix+"_PASSWORD") + } else if strings.HasPrefix(env, prefix+"_DATABASE") { + path = getEnvValue(env, prefix+"_DATABASE") + } else if strings.HasPrefix(env, prefix+"_VERSION") { + version = getEnvValue(env, prefix+"_VERSION") } } - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(p.PublicPort), - "username": username, - "password": password, - "rel": "amqp", - "scheme": "amqp", - } - // management plugin? - for _, pw := range exposedPorts { - if pw.PrivatePort == 15672 { - rels["-management"] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(pw.PublicPort), - "rel": "amqp", - "scheme": "http", - } - break - } + } + if path == "" { + path = username + } + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "username": username, + "password": password, + "path": path, + "version": version, + "port": formatDockerPort(p.PublicPort), + "query": map[string]bool{ + "is_master": true, + }, + "rel": "mysql", + "scheme": "mysql", + } + return rels + } else if p.PrivatePort == 5432 { + username := "" + password := "" + path := "" + version := "" + for _, env := range c.Config.Env { + if strings.HasPrefix(env, "POSTGRES_USER") { + username = getEnvValue(env, "POSTGRES_USER") + } else if strings.HasPrefix(env, "POSTGRES_PASSWORD") { + password = getEnvValue(env, "POSTGRES_PASSWORD") + } else if strings.HasPrefix(env, "POSTGRES_DB") { + path = getEnvValue(env, "POSTGRES_DB") + } else if strings.HasPrefix(env, "PG_VERSION") { + version = getEnvValue(env, "PG_VERSION") } - return rels - } else if p.PrivatePort == 9200 { - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(p.PublicPort), - "rel": "elasticsearch", - "scheme": "http", + } + if path == "" { + path = username + } + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "username": username, + "password": password, + "path": path, + "version": version, + "port": formatDockerPort(p.PublicPort), + "query": map[string]bool{ + "is_master": true, + }, + "rel": "pgsql", + "scheme": "pgsql", + } + return rels + } else if p.PrivatePort == 6379 { + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(p.PublicPort), + "rel": "redis", + "scheme": "redis", + } + return rels + } else if p.PrivatePort == 11211 { + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(p.PublicPort), + "rel": "memcached", + "scheme": "memcached", + } + return rels + } else if p.PrivatePort == 5672 { + username := "guest" + password := "guest" + for _, env := range c.Config.Env { + // that's our local convention + if strings.HasPrefix(env, "RABBITMQ_DEFAULT_USER") { + username = getEnvValue(env, "RABBITMQ_DEFAULT_USER") + } else if strings.HasPrefix(env, "RABBITMQ_DEFAULT_PASS") { + password = getEnvValue(env, "RABBITMQ_DEFAULT_PASS") } - return rels - } else if p.PrivatePort == 27017 { - path := "" - for _, env := range c.Config.Env { - // that's our local convention - if strings.HasPrefix(env, "MONGO_DATABASE") { - path = getEnvValue(env, "MONGO_DATABASE") + } + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(p.PublicPort), + "username": username, + "password": password, + "rel": "amqp", + "scheme": "amqp", + } + // management plugin? + for _, pw := range exposedPorts { + if pw.PrivatePort == 15672 { + rels["-management"] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(pw.PublicPort), + "rel": "amqp", + "scheme": "http", } + break } - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "username": "", - "password": "", - "path": path, - "port": formatDockerPort(p.PublicPort), - "rel": "mongodb", - "scheme": "mongodb", - } - return rels - } else if p.PrivatePort == 9092 { - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(p.PublicPort), - "rel": "kafka", - "scheme": "kafka", - } - return rels - } else if p.PrivatePort == 80 && container.Image == "dunglas/mercure" { - rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(p.PublicPort), - "rel": "mercure", - "scheme": "http", + } + return rels + } else if p.PrivatePort == 9200 { + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(p.PublicPort), + "path": "/", + "rel": "elasticsearch", + "scheme": "http", + } + return rels + } else if p.PrivatePort == 5601 { + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(p.PublicPort), + "path": "/", + "rel": "kibana", + "scheme": "http", + } + return rels + } else if p.PrivatePort == 27017 || p.PrivatePort == 27018 || p.PrivatePort == 27019 { + username := "" + password := "" + path := "" + for _, env := range c.Config.Env { + // that's our local convention + if strings.HasPrefix(env, "MONGO_DATABASE") { + path = getEnvValue(env, "MONGO_DATABASE") + } else if strings.HasPrefix(env, "MONGO_INITDB_DATABASE") { + path = getEnvValue(env, "MONGO_INITDB_DATABASE") + } else if strings.HasPrefix(env, "MONGO_INITDB_ROOT_USERNAME") { + username = getEnvValue(env, "MONGO_INITDB_ROOT_USERNAME") + } else if strings.HasPrefix(env, "MONGO_INITDB_ROOT_PASSWORD") { + password = getEnvValue(env, "MONGO_INITDB_ROOT_PASSWORD") } - return rels } - - if l.Debug { - fmt.Fprintln(os.Stderr, " exposing port") + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "username": username, + "password": password, + "path": path, + "port": formatDockerPort(p.PublicPort), + "rel": "mongodb", + "scheme": "mongodb", } - + return rels + } else if p.PrivatePort == 9092 { rels[""] = map[string]interface{}{ - "host": host, - "ip": host, - "port": formatDockerPort(p.PublicPort), - "rel": "simple", + "host": host, + "ip": host, + "port": formatDockerPort(p.PublicPort), + "rel": "kafka", + "scheme": "kafka", } return rels + } else if p.PrivatePort == 80 && strings.Contains(container.Image, "dunglas/mercure") { + // for podman the image name is docker.io/dunglas/mercure:latest + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(p.PublicPort), + "rel": "mercure", + "scheme": "http", + } + return rels + } + + if l.Debug { + fmt.Fprintln(os.Stderr, " exposing port") } - return nil + rels[""] = map[string]interface{}{ + "host": host, + "ip": host, + "port": formatDockerPort(p.PublicPort), + "rel": "simple", + } + // Official HTTP(s) ports or well know alternatives + if p.PrivatePort == 80 || p.PrivatePort == 8008 || p.PrivatePort == 8080 || p.PrivatePort == 8081 { + rels[""]["scheme"] = "http" + } else if p.PrivatePort == 443 || p.PrivatePort == 8443 { + rels[""]["scheme"] = "https" + } else { + rels[""]["scheme"] = "tcp" + } + return rels } func formatDockerPort(port uint16) string { return strconv.FormatInt(int64(port), 10) } +func dockerUseDesktopSocketIfAvailable(c *docker.Client) error { + if c.DaemonHost() != docker.DefaultDockerHost { + return nil + } + + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + + socketPath := filepath.Join(homeDir, ".docker/run/docker.sock") + if _, err := os.Stat(socketPath); err != nil { + return nil + } + + return docker.WithHost(`unix://` + socketPath)(c) +} + func getEnvValue(env string, key string) string { if len(key) == len(env) { return "" @@ -448,7 +511,7 @@ func getEnvValue(env string, key string) string { func (l *Local) getComposeProjectName() string { // https://docs.docker.com/compose/reference/envvars/#compose_project_name - if project := os.Getenv("COMPOSE_PROJECT_NAME"); project != "" { + if project := os.Getenv(composeConsts.ComposeProjectName); project != "" { return project } @@ -462,15 +525,38 @@ func (l *Local) getComposeProjectName() string { // COMPOSE_PROJECT_NAME can be set in a .env file if _, err := os.Stat(filepath.Join(composeDir, ".env")); err == nil { - if contents, err := ioutil.ReadFile(filepath.Join(composeDir, ".env")); err == nil { + if contents, err := os.ReadFile(filepath.Join(composeDir, ".env")); err == nil { for _, line := range bytes.Split(contents, []byte("\n")) { - if bytes.HasPrefix(line, []byte("COMPOSE_PROJECT_NAME=")) { - return string(line[len("COMPOSE_PROJECT_NAME="):]) + if bytes.HasPrefix(line, []byte(composeConsts.ComposeProjectName+"=")) { + return string(line[len(composeConsts.ComposeProjectName)+1:]) } } } } + // Compose project name can be set in every Docker Compose file + for index, filename := range compose.DefaultFileNames { + if _, err := os.Stat(filepath.Join(composeDir, filename)); err != nil { + continue + } + + for _, filename := range []string{compose.DefaultOverrideFileNames[index], filename} { + buf, err := os.ReadFile(filepath.Join(composeDir, filename)) + if err != nil { + continue + } + + config := struct { + ProjectName string `yaml:"name"` + }{} + + // unmarshall the content of the file to get the project name + if err := yaml.Unmarshal(buf, &config); err == nil && config.ProjectName != "" { + return config.ProjectName + } + } + } + return filepath.Base(composeDir) } @@ -483,17 +569,15 @@ func (l *Local) getComposeDir() string { // look for the first dir up with a docker-composer.ya?ml file (in case of a multi-project) dir := l.Dir for { - if _, err := os.Stat(filepath.Join(dir, "docker-compose.yaml")); err == nil { - return dir - } - // both .yml and .yaml are supported by Docker composer - if _, err := os.Stat(filepath.Join(dir, "docker-compose.yml")); err == nil { - return dir + for _, filename := range compose.DefaultFileNames { + if _, err := os.Stat(filepath.Join(dir, filename)); err == nil { + return dir + } } upDir := filepath.Dir(dir) if upDir == dir || upDir == "." { if l.Debug { - fmt.Fprintln(os.Stderr, "ERROR: unable to find a docker-compose.yaml or docker-compose.yml for the current directory") + fmt.Fprintln(os.Stderr, "ERROR: unable to find a docker-compose.ya?ml or compose.ya?ml for the current directory") } return "" } diff --git a/envs/docker_version.go b/envs/docker_version.go new file mode 100644 index 00000000..8b319b58 --- /dev/null +++ b/envs/docker_version.go @@ -0,0 +1,25 @@ +// Code generated by envs/generate_docker_version +// DO NOT EDIT + +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package envs + +const dockerClientVersion = "v28.0.0" diff --git a/envs/dotenv.go b/envs/dotenv.go index da2d9806..d648cf4c 100644 --- a/envs/dotenv.go +++ b/envs/dotenv.go @@ -50,6 +50,23 @@ func LoadDotEnv(vars map[string]string, scriptDir string) map[string]string { return vars } +// LookupEnv allows one to lookup for a single environment variable in the same +// way os.LookupEnv would. It automatically let the environment variable take +// over if defined. +func LookupEnv(dotEnvDir, key string) (string, bool) { + // first check if the user defined it in its environment + if value, isUserDefined := os.LookupEnv(key); isUserDefined { + return value, isUserDefined + } + + dotEnvEnv := lookupDotEnv(dotEnvDir) + if value, isDefined := dotEnvEnv[key]; isDefined { + return value, isDefined + } + + return "", false +} + // algorithm is here: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/3.3/config/bootstrap.php func lookupDotEnv(dir string) map[string]string { var err error diff --git a/envs/envs.go b/envs/envs.go index 50e99240..c36a2d1d 100644 --- a/envs/envs.go +++ b/envs/envs.go @@ -22,12 +22,13 @@ package envs import ( "encoding/json" "fmt" - "io/ioutil" "os" "path/filepath" + "regexp" "strconv" "strings" + "github.com/symfony-cli/symfony-cli/local/platformsh" "github.com/symfony-cli/symfony-cli/util" ) @@ -90,7 +91,7 @@ func AsMap(env Environment) map[string]string { // appID returns the Symfony project's ID from composer.json func appID(path string) string { - content, err := ioutil.ReadFile(filepath.Join(path, "composer.json")) + content, err := os.ReadFile(filepath.Join(path, "composer.json")) if err != nil { return "" } @@ -122,11 +123,11 @@ func extractRelationshipsEnvs(env Environment) Envs { } prefix = strings.Replace(prefix, "-", "_", -1) - if scheme == "pgsql" || scheme == "mysql" { - if !isMaster(endpoint) { - continue - } - if scheme == "pgsql" { + // HA support via scheme-replica + isPostgreSQL := strings.HasPrefix(scheme.(string), "pgsql") + isMySQL := strings.HasPrefix(scheme.(string), "mysql") + if isPostgreSQL || isMySQL { + if isPostgreSQL { // works for both Doctrine and Go endpoint["scheme"] = "postgres" } @@ -149,37 +150,55 @@ func extractRelationshipsEnvs(env Environment) Envs { } url += fmt.Sprintf("%s:%s/%s?sslmode=disable", endpoint["host"].(string), formatInt(endpoint["port"]), path) values[fmt.Sprintf("%sURL", prefix)] = url - if env.Language() != "golang" { + detectedLanguage := env.Language() + if detectedLanguage != "golang" { charset := "utf8" if envCharset := os.Getenv(fmt.Sprintf("%sCHARSET", prefix)); envCharset != "" { charset = envCharset - } else if scheme == "mysql" { + } else if isMySQL { charset = "utf8mb4" } values[fmt.Sprintf("%sURL", prefix)] = values[fmt.Sprintf("%sURL", prefix)] + "&charset=" + charset } - if env.Language() == "php" { + if detectedLanguage == "php" { + versionKey := fmt.Sprintf("%sVERSION", prefix) if v, ok := endpoint["type"]; ok { - versionKey := fmt.Sprintf("%sVERSION", prefix) - if version, hasVersionInEnv := os.LookupEnv(versionKey); hasVersionInEnv { - values[versionKey] = version - values[fmt.Sprintf("%sURL", prefix)] = values[fmt.Sprintf("%sURL", prefix)] + "&serverVersion=" + values[versionKey] - } else if strings.Contains(v.(string), ":") { - version := strings.SplitN(v.(string), ":", 2)[1] + // configuration from doctrine.yaml takes precedence over psh config + if doctrineConfigVersion, err := platformsh.ReadDBVersionFromDoctrineConfigYAML(env.Path()); err == nil && doctrineConfigVersion != "" { + // configuration from doctrine.yaml + values[versionKey] = doctrineConfigVersion + } else { + // type is available when in the cloud or locally via a tunnel + if version, hasVersionInEnv := os.LookupEnv(versionKey); hasVersionInEnv { + values[versionKey] = version + } else if strings.Contains(v.(string), ":") { + version := strings.SplitN(v.(string), ":", 2)[1] - // we actually provide mariadb not mysql - if endpoint["scheme"].(string) == "mysql" { - minor := 0 - if version == "10.2" { - minor = 7 + // we actually provide mariadb not mysql + if isMySQL { + minor := 0 + if version == "10.2" { + minor = 7 + } + version = fmt.Sprintf("%s.%d-MariaDB", version, minor) } - version = fmt.Sprintf("mariadb-%s.%d", version, minor) + + values[versionKey] = version } + } + } else if env.Local() { + // Docker support + if v, ok := endpoint["version"]; ok { + values[versionKey] = v.(string) - values[versionKey] = version - values[fmt.Sprintf("%sURL", prefix)] = values[fmt.Sprintf("%sURL", prefix)] + "&serverVersion=" + values[versionKey] + if matches := regexp.MustCompile(`^\d:(\d+\.\d+\.\d+).maria`).FindStringSubmatch(values[versionKey]); matches != nil { + values[versionKey] = fmt.Sprintf("%s-MariaDB", matches[1]) + } } } + if v, ok := values[versionKey]; ok && v != "" { + values[fmt.Sprintf("%sURL", prefix)] += "&serverVersion=" + v + } } values[fmt.Sprintf("%sSERVER", prefix)] = formatServer(endpoint) values[fmt.Sprintf("%sDRIVER", prefix)] = endpoint["scheme"].(string) @@ -189,13 +208,13 @@ func extractRelationshipsEnvs(env Environment) Envs { values[fmt.Sprintf("%sDATABASE", prefix)] = path if env.Local() { - if scheme == "pgsql" { + if isPostgreSQL { values["PGHOST"] = endpoint["host"].(string) values["PGPORT"] = formatInt(endpoint["port"]) values["PGDATABASE"] = path values["PGUSER"] = endpoint["username"].(string) values["PGPASSWORD"] = endpoint["password"].(string) - } else if scheme == "mysql" { + } else if isMySQL { values["MYSQL_HOST"] = endpoint["host"].(string) values["MYSQL_TCP_PORT"] = formatInt(endpoint["port"]) } @@ -211,7 +230,11 @@ func extractRelationshipsEnvs(env Environment) Envs { values[fmt.Sprintf("%sNAME", prefix)] = endpoint["path"].(string) values[fmt.Sprintf("%sDATABASE", prefix)] = endpoint["path"].(string) } else if rel == "elasticsearch" { - values[fmt.Sprintf("%sURL", prefix)] = fmt.Sprintf("%s://%s:%s", endpoint["scheme"].(string), endpoint["host"].(string), formatInt(endpoint["port"])) + path, hasPath := endpoint["path"] + if !hasPath || path == nil { + path = "" + } + values[fmt.Sprintf("%sURL", prefix)] = fmt.Sprintf("%s://%s:%s%s", endpoint["scheme"].(string), endpoint["host"].(string), formatInt(endpoint["port"]), path) values[fmt.Sprintf("%sHOST", prefix)] = endpoint["host"].(string) values[fmt.Sprintf("%sPORT", prefix)] = formatInt(endpoint["port"]) values[fmt.Sprintf("%sSCHEME", prefix)] = endpoint["scheme"].(string) @@ -219,18 +242,25 @@ func extractRelationshipsEnvs(env Environment) Envs { if !isMaster(endpoint) { continue } + values[fmt.Sprintf("%sURL", prefix)] = fmt.Sprintf("%s://%s:%s@%s:%s/?authSource=%s", endpoint["scheme"].(string), endpoint["username"].(string), endpoint["password"].(string), endpoint["host"].(string), formatInt(endpoint["port"]), endpoint["path"].(string)) values[fmt.Sprintf("%sSERVER", prefix)] = formatServer(endpoint) values[fmt.Sprintf("%sHOST", prefix)] = endpoint["host"].(string) values[fmt.Sprintf("%sPORT", prefix)] = formatInt(endpoint["port"]) values[fmt.Sprintf("%sSCHEME", prefix)] = endpoint["scheme"].(string) values[fmt.Sprintf("%sNAME", prefix)] = endpoint["path"].(string) values[fmt.Sprintf("%sDATABASE", prefix)] = endpoint["path"].(string) + values[fmt.Sprintf("%sDB", prefix)] = endpoint["path"].(string) values[fmt.Sprintf("%sUSER", prefix)] = endpoint["username"].(string) values[fmt.Sprintf("%sUSERNAME", prefix)] = endpoint["username"].(string) values[fmt.Sprintf("%sPASSWORD", prefix)] = endpoint["password"].(string) } else if scheme == "amqp" { - values[fmt.Sprintf("%sURL", prefix)] = fmt.Sprintf("%s://%s:%s@%s:%s", endpoint["scheme"].(string), endpoint["username"].(string), endpoint["password"].(string), endpoint["host"].(string), formatInt(endpoint["port"])) - values[fmt.Sprintf("%sDSN", prefix)] = fmt.Sprintf("%s://%s:%s@%s:%s", endpoint["scheme"].(string), endpoint["username"].(string), endpoint["password"].(string), endpoint["host"].(string), formatInt(endpoint["port"])) + vhost := "" + if v, ok := endpoint["vhost"]; ok && v != nil { + values[fmt.Sprintf("%sVHOST", prefix)] = endpoint["vhost"].(string) + vhost = "/" + endpoint["vhost"].(string) + } + values[fmt.Sprintf("%sURL", prefix)] = fmt.Sprintf("%s://%s:%s@%s:%s%s", endpoint["scheme"].(string), endpoint["username"].(string), endpoint["password"].(string), endpoint["host"].(string), formatInt(endpoint["port"]), vhost) + values[fmt.Sprintf("%sDSN", prefix)] = fmt.Sprintf("%s://%s:%s@%s:%s%s", endpoint["scheme"].(string), endpoint["username"].(string), endpoint["password"].(string), endpoint["host"].(string), formatInt(endpoint["port"]), vhost) values[fmt.Sprintf("%sSERVER", prefix)] = formatServer(endpoint) values[fmt.Sprintf("%sHOST", prefix)] = endpoint["host"].(string) values[fmt.Sprintf("%sPORT", prefix)] = formatInt(endpoint["port"]) @@ -241,21 +271,29 @@ func extractRelationshipsEnvs(env Environment) Envs { } else if scheme == "memcached" { values[fmt.Sprintf("%sHOST", prefix)] = endpoint["host"].(string) values[fmt.Sprintf("%sPORT", prefix)] = formatInt(endpoint["port"]) - values[fmt.Sprintf("%sIP", prefix)] = endpoint["ip"].(string) + if v, ok := endpoint["ip"]; ok && v != nil { + values[fmt.Sprintf("%sIP", prefix)] = v.(string) + } } else if rel == "influxdb" { values[fmt.Sprintf("%sSCHEME", prefix)] = endpoint["scheme"].(string) values[fmt.Sprintf("%sHOST", prefix)] = endpoint["host"].(string) values[fmt.Sprintf("%sPORT", prefix)] = formatInt(endpoint["port"]) - values[fmt.Sprintf("%sIP", prefix)] = endpoint["ip"].(string) + if v, ok := endpoint["ip"]; ok && v != nil { + values[fmt.Sprintf("%sIP", prefix)] = v.(string) + } } else if scheme == "kafka" { values[fmt.Sprintf("%sURL", prefix)] = fmt.Sprintf("%s://%s:%s", endpoint["scheme"].(string), endpoint["host"].(string), formatInt(endpoint["port"])) values[fmt.Sprintf("%sSCHEME", prefix)] = endpoint["scheme"].(string) values[fmt.Sprintf("%sHOST", prefix)] = endpoint["host"].(string) values[fmt.Sprintf("%sPORT", prefix)] = formatInt(endpoint["port"]) - values[fmt.Sprintf("%sIP", prefix)] = endpoint["ip"].(string) + if v, ok := endpoint["ip"]; ok && v != nil { + values[fmt.Sprintf("%sIP", prefix)] = v.(string) + } } else if scheme == "tcp" { values[fmt.Sprintf("%sURL", prefix)] = formatServer(endpoint) - values[fmt.Sprintf("%sIP", prefix)] = endpoint["ip"].(string) + if v, ok := endpoint["ip"]; ok && v != nil { + values[fmt.Sprintf("%sIP", prefix)] = v.(string) + } values[fmt.Sprintf("%sPORT", prefix)] = formatInt(endpoint["port"]) values[fmt.Sprintf("%sSCHEME", prefix)] = endpoint["scheme"].(string) values[fmt.Sprintf("%sHOST", prefix)] = endpoint["host"].(string) @@ -265,7 +303,7 @@ func extractRelationshipsEnvs(env Environment) Envs { } else if rel == "mercure" { values["MERCURE_URL"] = fmt.Sprintf("%s://%s:%s/.well-known/mercure", endpoint["scheme"].(string), endpoint["host"].(string), formatInt(endpoint["port"])) values["MERCURE_PUBLIC_URL"] = values["MERCURE_URL"] - } else if scheme == "http" { + } else if scheme == "http" || scheme == "https" { username, hasUsername := endpoint["username"].(string) password, hasPassword := endpoint["password"].(string) if hasUsername || hasPassword { @@ -274,7 +312,9 @@ func extractRelationshipsEnvs(env Environment) Envs { values[fmt.Sprintf("%sURL", prefix)] = fmt.Sprintf("%s://%s:%s", endpoint["scheme"].(string), endpoint["host"].(string), formatInt(endpoint["port"])) } values[fmt.Sprintf("%sSERVER", prefix)] = formatServer(endpoint) - values[fmt.Sprintf("%sIP", prefix)] = endpoint["ip"].(string) + if v, ok := endpoint["ip"]; ok && v != nil { + values[fmt.Sprintf("%sIP", prefix)] = v.(string) + } values[fmt.Sprintf("%sPORT", prefix)] = formatInt(endpoint["port"]) values[fmt.Sprintf("%sSCHEME", prefix)] = endpoint["scheme"].(string) values[fmt.Sprintf("%sHOST", prefix)] = endpoint["host"].(string) @@ -301,7 +341,9 @@ func extractRelationshipsEnvs(env Environment) Envs { // for Symfony Mailer, use a MAILER prefix values[fmt.Sprintf("%sDSN", prefix)] = fmt.Sprintf("%s://%s:%s", endpoint["scheme"].(string), endpoint["host"].(string), formatInt(endpoint["port"])) } else if rel == "simple" { - values[fmt.Sprintf("%sIP", prefix)] = endpoint["ip"].(string) + if v, ok := endpoint["ip"]; ok && v != nil { + values[fmt.Sprintf("%sIP", prefix)] = v.(string) + } values[fmt.Sprintf("%sPORT", prefix)] = formatInt(endpoint["port"]) values[fmt.Sprintf("%sHOST", prefix)] = endpoint["host"].(string) } @@ -318,6 +360,9 @@ func formatInt(val interface{}) string { if s, ok := val.(string); ok { return s } + if i, ok := val.(int); ok { + return strconv.Itoa(i) + } return strconv.FormatInt(int64(val.(float64)), 10) } diff --git a/envs/envs_test.go b/envs/envs_test.go index 3edaf3b5..d02a62c5 100644 --- a/envs/envs_test.go +++ b/envs/envs_test.go @@ -37,3 +37,284 @@ func (s *ScenvSuite) TestAppID(c *C) { c.Assert(appID("testdata/project_without_composer"), Equals, "") c.Assert(appID("testdata/project_with_borked_composer"), Equals, "") } + +type fakeEnv struct { + Rels Relationships + RootPath string +} + +func (f fakeEnv) Path() string { + if f.RootPath != "" { + return f.RootPath + } + return "/dev/null" +} + +func (f fakeEnv) Mailer() Envs { + return nil +} + +func (f fakeEnv) Language() string { + return "php" +} + +func (f fakeEnv) Relationships() Relationships { + return f.Rels +} + +func (f fakeEnv) Extra() Envs { + return nil +} + +func (f fakeEnv) Local() bool { + return true +} + +func (s *ScenvSuite) TestElasticsearchURLEndsWithTrailingSlash(c *C) { + env := fakeEnv{ + Rels: map[string][]map[string]interface{}{ + "elasticsearch": { + map[string]interface{}{ + "host": "localhost", + "ip": "localhost", + "port": 9200, + "path": "/", + "rel": "elasticsearch", + "scheme": "http", + }, + }, + }, + } + + rels := extractRelationshipsEnvs(env) + c.Assert(rels["ELASTICSEARCH_URL"], Equals, "http://localhost:9200/") + + // We want to stay backward compatible with Platform.sh/SymfonyCloud + env.Rels["elasticsearch"][0]["path"] = nil + rels = extractRelationshipsEnvs(env) + c.Assert(rels["ELASTICSEARCH_URL"], Equals, "http://localhost:9200") + + delete(env.Rels["elasticsearch"][0], "path") + rels = extractRelationshipsEnvs(env) + c.Assert(rels["ELASTICSEARCH_URL"], Equals, "http://localhost:9200") +} + +func (s *ScenvSuite) TestDockerDatabaseURLs(c *C) { + env := fakeEnv{ + Rels: map[string][]map[string]interface{}{ + "mysql": { + map[string]interface{}{ + "host": "127.0.0.1", + "ip": "127.0.0.1", + "password": "!ChangeMe!", + "path": "root", + "port": "56614", + "query": map[string]bool{"is_master": true}, + "rel": "mysql", + "scheme": "mysql", + "username": "root", + "version": "1:10.0.38+maria-1~xenial", + }, + }, + "postgresql": { + map[string]interface{}{ + "host": "127.0.0.1", + "ip": "127.0.0.1", + "password": "main", + "path": "main", + "port": "63574", + "query": map[string]bool{"is_master": true}, + "rel": "pgsql", + "scheme": "pgsql", + "username": "main", + "version": "13.13", + }, + }, + }, + } + + rels := extractRelationshipsEnvs(env) + c.Assert(rels["MYSQL_URL"], Equals, "mysql://root:!ChangeMe!@127.0.0.1:56614/root?sslmode=disable&charset=utf8mb4&serverVersion=10.0.38-MariaDB") + c.Assert(rels["POSTGRESQL_URL"], Equals, "postgres://main:main@127.0.0.1:63574/main?sslmode=disable&charset=utf8&serverVersion=13.13") +} + +func (s *ScenvSuite) TestCloudTunnelDatabaseURLs(c *C) { + env := fakeEnv{ + Rels: map[string][]map[string]interface{}{ + "mysql": { + { + "cluster": "d3xkaapt4cyik-main-bvxea6i", + "epoch": 0, + "fragment": interface{}(nil), + "host": "127.0.0.1", + "host_mapped": false, + "hostname": "vd4wb3toqpyybms2qktcjmdng4.database.service._.eu-5.platformsh.site", + "instance_ips": []interface{}{"249.175.144.213"}, + "ip": "127.0.0.1", + "password": "", + "path": "main", + "port": "30001", + "public": false, + "query": map[string]interface{}{"is_master": true}, + "rel": "mysql", + "scheme": "mysql", + "service": "database", + "type": "mysql:10.0", + "username": "user", + }, + }, + "postgresql": { + { + "cluster": "xxx-master-yyy", + "epoch": 0, + "fragment": interface{}(nil), + "host": "127.0.0.1", + "host_mapped": false, + "hostname": "xxx.pgsqldb.service._.fr-4.platformsh.site", + "instance_ips": []interface{}{"240.7.208.71"}, + "ip": "127.0.0.1", + "password": "main", + "path": "main", + "port": "30000", + "public": false, + "query": map[string]interface{}{"is_master": true}, + "rel": "postgresql", + "scheme": "pgsql", + "service": "pgsqldb", + "type": "postgresql:13", + "username": "main", + }, + }, + }, + } + + rels := extractRelationshipsEnvs(env) + c.Assert(rels["MYSQL_URL"], Equals, "mysql://user@127.0.0.1:30001/main?sslmode=disable&charset=utf8mb4&serverVersion=10.0.0-MariaDB") + c.Assert(rels["POSTGRESQL_URL"], Equals, "postgres://main:main@127.0.0.1:30000/main?sslmode=disable&charset=utf8&serverVersion=13") +} + +func (s *ScenvSuite) TestCloudHADatabaseURLs(c *C) { + env := fakeEnv{ + Rels: map[string][]map[string]interface{}{ + "database-replica": { + { + "username": "user", + "scheme": "mysql", + "service": "db", + "fragment": interface{}(nil), + "ip": "169.254.150.110", + "hostname": "e3n2frcxjqipslsc6sq7rfmwzm.db.service.._.platform.sh", + "port": 3306, + "cluster": "gqiujktuqrcxm-main-bvxea6i", + "host": "database-replica.internal", + "rel": "mysql-replica", + "path": "main", + "query": map[string]interface{}{"is_master": false}, + "password": "", + "type": "mysql:10.6", + "public": false, + "host_mapped": false, + }, + }, + "database": { + { + "username": "user", + "scheme": "mysql", + "service": "db", + "fragment": interface{}(nil), + "ip": "169.254.193.18", + "hostname": "jvlu7c7jx3nzt3cowwkcrslhcq.db.service.._.platform.sh", + "port": 3306, + "cluster": "gqiujktuqrcxm-main-bvxea6i", + "host": "database.internal", + "rel": "mysql", + "path": "main", + "query": map[string]interface{}{"is_master": true}, + "password": "", + "type": "mysql:10.6", + "public": false, + "host_mapped": false, + }, + }, + "psql-replica": { + { + "username": "user", + "scheme": "pgsql", + "service": "db", + "fragment": interface{}(nil), + "ip": "169.254.150.110", + "hostname": "e3n2frcxjqipslsc6sq7rfmwzm.db.service.._.platform.sh", + "port": 5432, + "cluster": "gqiujktuqrcxm-main-bvxea6i", + "host": "psql-replica.internal", + "rel": "pgsql-replica", + "path": "main", + "query": map[string]interface{}{"is_master": false}, + "password": "", + "type": "postgresql:15", + "public": false, + "host_mapped": false, + }, + }, + "psql": { + { + "username": "user", + "scheme": "pgsql", + "service": "db", + "fragment": interface{}(nil), + "ip": "169.254.193.18", + "hostname": "jvlu7c7jx3nzt3cowwkcrslhcq.db.service.._.platform.sh", + "port": 5432, + "cluster": "gqiujktuqrcxm-main-bvxea6i", + "host": "psql.internal", + "rel": "pgsql", + "path": "main", + "query": map[string]interface{}{"is_master": true}, + "password": "", + "type": "postgresql:15", + "public": false, + "host_mapped": false, + }, + }, + }, + } + + rels := extractRelationshipsEnvs(env) + c.Assert(rels["DATABASE_URL"], Equals, "mysql://user@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.6.0-MariaDB") + c.Assert(rels["DATABASE_REPLICA_URL"], Equals, "mysql://user@database-replica.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.6.0-MariaDB") + c.Assert(rels["PSQL_URL"], Equals, "postgres://user@psql.internal:5432/main?sslmode=disable&charset=utf8&serverVersion=15") + c.Assert(rels["PSQL_REPLICA_URL"], Equals, "postgres://user@psql-replica.internal:5432/main?sslmode=disable&charset=utf8&serverVersion=15") +} + +func (s *ScenvSuite) TestDoctrineConfigTakesPrecedenceDatabaseURLs(c *C) { + env := fakeEnv{ + Rels: map[string][]map[string]interface{}{ + "mysql": { + { + "cluster": "d3xkaapt4cyik-main-bvxea6i", + "epoch": 0, + "fragment": interface{}(nil), + "host": "127.0.0.1", + "host_mapped": false, + "hostname": "vd4wb3toqpyybms2qktcjmdng4.database.service._.eu-5.platformsh.site", + "instance_ips": []interface{}{"249.175.144.213"}, + "ip": "127.0.0.1", + "password": "", + "path": "main", + "port": "30001", + "public": false, + "query": map[string]interface{}{"is_master": true}, + "rel": "mysql", + "scheme": "mysql", + "service": "database", + "type": "mysql:10.0", + "username": "user", + }, + }, + }, + RootPath: "testdata/doctrine-project", + } + + rels := extractRelationshipsEnvs(env) + c.Assert(rels["MYSQL_URL"], Equals, "mysql://user@127.0.0.1:30001/main?sslmode=disable&charset=utf8mb4&serverVersion=8.0.33") +} diff --git a/envs/generate_docker_version b/envs/generate_docker_version new file mode 100644 index 00000000..b5b42fb7 --- /dev/null +++ b/envs/generate_docker_version @@ -0,0 +1,29 @@ +#!/usr/bin/env sh + +cat < docker_version.go +// Code generated by envs/generate_docker_version +// DO NOT EDIT + +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package envs + +const dockerClientVersion = "$(go list -m all | grep github.com/docker/docker | awk -F '[ +]' '{print $2}')" +EOF diff --git a/envs/local.go b/envs/local.go index 40eb3c82..46f5a3bc 100644 --- a/envs/local.go +++ b/envs/local.go @@ -75,6 +75,73 @@ func (l *Local) FindRelationshipPrefix(frel, fscheme string) string { return "" } +func (l *Local) FindHttpServices() []string { + services := []string{} + + for key, endpoints := range l.Relationships() { + for _, endpoint := range endpoints { + if scheme, ok := endpoint["scheme"].(string); !ok { + continue + } else if scheme != "http" && scheme != "https" { + continue + } + + services = append(services, key) + } + } + + return services +} + +func (l *Local) FindServiceUrl(serviceOrRelationship string) (string, bool) { + relationships := l.Relationships() + env := AsMap(l) + + if endpoints, serviceIsDefined := relationships[serviceOrRelationship]; serviceIsDefined { + for i, endpoint := range endpoints { + if scheme, ok := endpoint["scheme"].(string); !ok { + continue + } else if scheme != "http" && scheme != "https" { + continue + } + + prefix := fmt.Sprintf("%s_", strings.Replace(strings.ToUpper(serviceOrRelationship), "-", "_", -1)) + if i != 0 { + prefix += fmt.Sprintf("%d_", i) + } + + if url, exists := env[prefix+"URL"]; exists { + return url, true + } + } + } + + for key, endpoints := range relationships { + for i, endpoint := range endpoints { + if endpoint["rel"].(string) != serviceOrRelationship { + continue + } + + if scheme, ok := endpoint["scheme"].(string); !ok { + continue + } else if scheme != "http" && scheme != "https" { + continue + } + + prefix := fmt.Sprintf("%s_", strings.Replace(strings.ToUpper(key), "-", "_", -1)) + if i != 0 { + prefix += fmt.Sprintf("%d_", i) + } + + if url, exists := env[prefix+"URL"]; exists { + return url, true + } + } + } + + return "", false +} + // Path returns the project's path func (l *Local) Path() string { return l.Dir @@ -93,7 +160,9 @@ func (l *Local) Relationships() Relationships { project, err := platformsh.ProjectFromDir(l.Dir, l.Debug) if err != nil { if l.Debug { - fmt.Fprint(os.Stderr, "ERROR: unable to get Platform.sh project information\n") + if brand := platformsh.GuessCloudFromDirectory(l.Dir); brand != platformsh.NoBrand { + fmt.Fprintf(os.Stderr, "ERROR: unable to get %s project information\n", brand) + } } return dockerRel } @@ -128,9 +197,10 @@ func (l *Local) Extra() Envs { sc = "1" } env := Envs{ - "SYMFONY_TUNNEL": l.Tunnel, - "SYMFONY_TUNNEL_ENV": sc, - "SYMFONY_DOCKER_ENV": docker, + "SYMFONY_TUNNEL": l.Tunnel, + "SYMFONY_TUNNEL_ENV": sc, + "SYMFONY_TUNNEL_BRAND": platformsh.GuessCloudFromDirectory(l.Dir).Name, + "SYMFONY_DOCKER_ENV": docker, } if _, err := os.Stat(filepath.Join(l.Dir, ".prod")); err == nil { env["APP_ENV"] = "prod" @@ -159,7 +229,9 @@ func (l *Local) Language() string { app := platformsh.GuessSelectedAppByWd(platformsh.FindLocalApplications(projectRoot)) if app == nil { if l.Debug { - fmt.Fprint(os.Stderr, "ERROR: unable to get project configuration\n") + if platformsh.GuessCloudFromDirectory(l.Dir) != platformsh.NoBrand { + fmt.Fprint(os.Stderr, "ERROR: unable to get project configuration\n") + } } return "php" } @@ -187,15 +259,15 @@ func (l *Local) webServer() Envs { host := fmt.Sprintf("127.0.0.1:%s", port) if proxyConf, err := proxy.Load(util.GetHomeDir()); err == nil { - for _, domain := range proxyConf.GetDomains(l.Dir) { + domains := proxyConf.GetDomains(l.Dir) + if len(domains) > 0 { // we get the first one only - host = domain + host = domains[0] if pidFile.Scheme == "http" { port = "80" } else { port = "443" } - break } } diff --git a/envs/local_test.go b/envs/local_test.go index d3d635a3..68fd5163 100644 --- a/envs/local_test.go +++ b/envs/local_test.go @@ -35,9 +35,20 @@ var _ = Suite(&LocalSuite{}) func (s *LocalSuite) TestExtra(c *C) { l := &Local{} c.Assert(l.Extra(), DeepEquals, Envs{ - "SYMFONY_TUNNEL": "", - "SYMFONY_TUNNEL_ENV": "", - "SYMFONY_DOCKER_ENV": "", + "SYMFONY_TUNNEL": "", + "SYMFONY_TUNNEL_ENV": "", + "SYMFONY_TUNNEL_BRAND": "", + "SYMFONY_DOCKER_ENV": "", + }) + + l = &Local{ + Dir: "testdata/upsun", + } + c.Assert(l.Extra(), DeepEquals, Envs{ + "SYMFONY_TUNNEL": "", + "SYMFONY_TUNNEL_ENV": "", + "SYMFONY_TUNNEL_BRAND": "Upsun", + "SYMFONY_DOCKER_ENV": "", }) } @@ -47,7 +58,7 @@ func (s *LocalSuite) TestTunnelFilePath(c *C) { defer func() { os.Rename("testdata/project/.git", "testdata/project/git") }() - project, err := platformsh.ProjectFromDir(l.Dir, false) + project, err := platformsh.ProjectFromDir(l.Dir, true) if err != nil { panic(err) } diff --git a/envs/local_tunnel.go b/envs/local_tunnel.go index 452bb820..3a93ae0e 100644 --- a/envs/local_tunnel.go +++ b/envs/local_tunnel.go @@ -23,7 +23,6 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" "os" "path" "path/filepath" @@ -45,10 +44,11 @@ type pshtunnel struct { } func (l *Local) relationshipsFromTunnel() Relationships { + brand := platformsh.GuessCloudFromDirectory(l.Dir) project, err := platformsh.ProjectFromDir(l.Dir, l.Debug) if err != nil { if l.Debug { - fmt.Fprintf(os.Stderr, "WARNING: unable to detect Platform.sh project: %s\n", err) + fmt.Fprintf(os.Stderr, "WARNING: unable to detect %s project: %s\n", brand, err) } return nil } @@ -57,8 +57,8 @@ func (l *Local) relationshipsFromTunnel() Relationships { if err != nil { userHomeDir = "" } - tunnelFile := filepath.Join(userHomeDir, ".platformsh", "tunnel-info.json") - data, err := ioutil.ReadFile(tunnelFile) + tunnelFile := filepath.Join(userHomeDir, brand.CLIConfigPath, "tunnel-info.json") + data, err := os.ReadFile(tunnelFile) if err != nil { if l.Debug { fmt.Fprintf(os.Stderr, "WARNING: unable to read relationships from %s: %s\n", tunnelFile, err) @@ -98,7 +98,7 @@ func (l *Local) relationshipsFromTunnel() Relationships { return nil } -var pathCleaningRegex = regexp.MustCompile("[^a-zA-Z0-9-\\.]+") +var pathCleaningRegex = regexp.MustCompile(`[^a-zA-Z0-9-\.]+`) type Tunnel struct { Project *platformsh.Project diff --git a/envs/remote_test.go b/envs/remote_test.go index b3d511de..3e1d648c 100644 --- a/envs/remote_test.go +++ b/envs/remote_test.go @@ -20,6 +20,7 @@ package envs import ( + "encoding/base64" "os" . "gopkg.in/check.v1" @@ -256,6 +257,7 @@ func (s *RemoteSuite) TestDefaultRoute(c *C) { func (s *RemoteSuite) TestRelationships(c *C) { r := &Remote{} + os.Setenv("PLATFORM_RELATIONSHIPS", "") c.Assert(extractRelationshipsEnvs(r), DeepEquals, Envs{}) os.Setenv("PLATFORM_RELATIONSHIPS", "eyJzZWN1cml0eS1zZXJ2ZXIiOiBbeyJpcCI6ICIxNjkuMjU0LjI2LjIzMSIsICJob3N0IjogInNlY3VyaXR5LXNlcnZlci5pbnRlcm5hbCIsICJzY2hlbWUiOiAiaHR0cCIsICJwb3J0IjogODAsICJyZWwiOiAiaHR0cCJ9XSwgImRhdGFiYXNlIjogW3sidXNlcm5hbWUiOiAibWFpbiIsICJzY2hlbWUiOiAicGdzcWwiLCAiaXAiOiAiMTY5LjI1NC4xMjAuNDgiLCAiaG9zdCI6ICJkYXRhYmFzZS5pbnRlcm5hbCIsICJyZWwiOiAicG9zdGdyZXNxbCIsICJwYXRoIjogIm1haW4iLCAicXVlcnkiOiB7ImlzX21hc3RlciI6IHRydWV9LCAicGFzc3dvcmQiOiAibWFpbiIsICJwb3J0IjogNTQzMn1dfQ==") @@ -315,7 +317,7 @@ func (s *RemoteSuite) TestRelationships(c *C) { os.Setenv("PLATFORM_RELATIONSHIPS", "eyJyZWRpcyI6IFt7InVzZXJuYW1lIjogbnVsbCwgInBhc3N3b3JkIjogbnVsbCwgInNlcnZpY2UiOiAicmVkaXNfc2Vzc2lvbnMiLCAiZnJhZ21lbnQiOiBudWxsLCAiaXAiOiAiMTY5LjI1NC4yMy4yMDQiLCAiaG9zdG5hbWUiOiAidGVncTdqY3BqMjVuM3VnYjN0cm9rY2w1anEucmVkaXNfc2Vzc2lvbnMuc2VydmljZS5fLnM1eS5pbyIsICJwb3J0IjogNjM3OSwgImNsdXN0ZXIiOiAiN2NhbTRtbTUzN2ViZS1jbGVhbi11cC10bWVpd2hxIiwgImhvc3QiOiAicmVkaXMuaW50ZXJuYWwiLCAicmVsIjogInJlZGlzIiwgInBhdGgiOiBudWxsLCAicXVlcnkiOiB7fSwgInNjaGVtZSI6ICJyZWRpcyIsICJ0eXBlIjogInJlZGlzOjMuMiIsICJwdWJsaWMiOiBmYWxzZX1dLCAiZGF0YWJhc2UiOiBbeyJ1c2VybmFtZSI6ICJtYWluIiwgInBhc3N3b3JkIjogIjZlNjAyODg4NTc2NzAzMDMwZjUzYzE1NDA1MWJkNzc4IiwgInNlcnZpY2UiOiAibXlzcWwiLCAiaXAiOiAiMTY5LjI1NC4xMzQuMTEiLCAiaG9zdG5hbWUiOiAiaTNvNjJkbzV0eXh5MzV3NXdzdTY1YmdjcnUubXlzcWwuc2VydmljZS5fLnM1eS5pbyIsICJjbHVzdGVyIjogIjdjYW00bW01MzdlYmUtY2xlYW4tdXAtdG1laXdocSIsICJob3N0IjogImRhdGFiYXNlLmludGVybmFsIiwgInJlbCI6ICJtYWluIiwgInF1ZXJ5IjogeyJpc19tYXN0ZXIiOiB0cnVlfSwgInBhdGgiOiAibWFpbiIsICJzY2hlbWUiOiAibXlzcWwiLCAidHlwZSI6ICJteXNxbDoxMC4wIiwgInBvcnQiOiAzMzA2fV19") c.Assert(extractRelationshipsEnvs(r), DeepEquals, Envs{ - "DATABASE_URL": "mysql://main:6e602888576703030f53c154051bd778@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=mariadb-10.0.0", + "DATABASE_URL": "mysql://main:6e602888576703030f53c154051bd778@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.0.0-MariaDB", "DATABASE_DRIVER": "mysql", "DATABASE_NAME": "main", "DATABASE_DATABASE": "main", @@ -325,7 +327,7 @@ func (s *RemoteSuite) TestRelationships(c *C) { "DATABASE_USER": "main", "DATABASE_USERNAME": "main", "DATABASE_PASSWORD": "6e602888576703030f53c154051bd778", - "DATABASE_VERSION": "mariadb-10.0.0", + "DATABASE_VERSION": "10.0.0-MariaDB", "REDIS_URL": "redis://redis.internal:6379", "REDIS_HOST": "redis.internal", "REDIS_PORT": "6379", @@ -367,12 +369,12 @@ func (s *RemoteSuite) TestRelationships(c *C) { os.Setenv("PLATFORM_RELATIONSHIPS", "eyJkYXRhYmFzZSI6IFt7InNlcnZpY2UiOiAiZGF0YWJhc2VfbXlzcWxfbWFpbiIsICJpcCI6ICIxNjkuMjU0LjE4MC4yMjUiLCAiaG9zdG5hbWUiOiAibG03aDJld2Zvemc2cHVyamlhYmI1ZzRpYnUuZGF0YWJhc2VfbXlzcWxfbWFpbi5zZXJ2aWNlLl8uczV5LmlvIiwgImNsdXN0ZXIiOiAieHBzZm9vdGFwanc0YS1tYXN0ZXItN3JxdHd0aSIsICJob3N0IjogImRhdGFiYXNlLmludGVybmFsIiwgInJlbCI6ICJteXNxbCIsICJxdWVyeSI6IHt9LCAic2NoZW1lIjogIm15c3FsIiwgInR5cGUiOiAibXlzcWw6MTAuMSIsICJwb3J0IjogMzMwNn1dfQ==") c.Assert(extractRelationshipsEnvs(r), DeepEquals, Envs{ - "DATABASE_URL": "mysql://database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=mariadb-10.1.0", + "DATABASE_URL": "mysql://database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.1.0-MariaDB", "DATABASE_SERVER": "mysql://database.internal:3306", "DATABASE_DRIVER": "mysql", "DATABASE_HOST": "database.internal", "DATABASE_PORT": "3306", - "DATABASE_VERSION": "mariadb-10.1.0", + "DATABASE_VERSION": "10.1.0-MariaDB", "DATABASE_DATABASE": "main", "DATABASE_NAME": "main", }) @@ -380,26 +382,91 @@ func (s *RemoteSuite) TestRelationships(c *C) { os.Setenv("PLATFORM_RELATIONSHIPS", "eyJkYXRhYmFzZSI6IFt7InNlcnZpY2UiOiAiZGF0YWJhc2VfbXlzcWxfbWFpbiIsICJpcCI6ICIxNjkuMjU0LjE4MC4yMjUiLCAiaG9zdG5hbWUiOiAibG03aDJld2Zvemc2cHVyamlhYmI1ZzRpYnUuZGF0YWJhc2VfbXlzcWxfbWFpbi5zZXJ2aWNlLl8uczV5LmlvIiwgImNsdXN0ZXIiOiAieHBzZm9vdGFwanc0YS1tYXN0ZXItN3JxdHd0aSIsICJob3N0IjogImRhdGFiYXNlLmludGVybmFsIiwgInJlbCI6ICJteXNxbCIsICJxdWVyeSI6IHt9LCAic2NoZW1lIjogIm15c3FsIiwgInR5cGUiOiAibXlzcWw6MTAuMiIsICJwb3J0IjogMzMwNn1dfQo=") // Maria DB 10.2 == 10.2.7 c.Assert(extractRelationshipsEnvs(r), DeepEquals, Envs{ - "DATABASE_URL": "mysql://database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=mariadb-10.2.7", + "DATABASE_URL": "mysql://database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.2.7-MariaDB", "DATABASE_SERVER": "mysql://database.internal:3306", "DATABASE_DRIVER": "mysql", "DATABASE_HOST": "database.internal", "DATABASE_PORT": "3306", - "DATABASE_VERSION": "mariadb-10.2.7", + "DATABASE_VERSION": "10.2.7-MariaDB", "DATABASE_DATABASE": "main", "DATABASE_NAME": "main", }) - os.Setenv("DATABASE_VERSION", "mariadb-10.2.19") + os.Setenv("DATABASE_VERSION", "10.2.19-MariaDB") os.Setenv("DATABASE_CHARSET", "utf8") c.Assert(extractRelationshipsEnvs(r), DeepEquals, Envs{ - "DATABASE_URL": "mysql://database.internal:3306/main?sslmode=disable&charset=utf8&serverVersion=mariadb-10.2.19", + "DATABASE_URL": "mysql://database.internal:3306/main?sslmode=disable&charset=utf8&serverVersion=10.2.19-MariaDB", "DATABASE_SERVER": "mysql://database.internal:3306", "DATABASE_DRIVER": "mysql", "DATABASE_HOST": "database.internal", "DATABASE_PORT": "3306", - "DATABASE_VERSION": "mariadb-10.2.19", + "DATABASE_VERSION": "10.2.19-MariaDB", "DATABASE_DATABASE": "main", "DATABASE_NAME": "main", }) + os.Unsetenv("DATABASE_VERSION") + os.Unsetenv("DATABASE_CHARSET") +} + +func (s *RemoteSuite) TestMySQLReadReplicaForDedicated(c *C) { + r := &Remote{} + value, err := os.ReadFile("testdata/dedicated/relationships_with_read_replica.json") + if err != nil { + panic(err) + } + if err := os.Setenv("PLATFORM_RELATIONSHIPS", base64.StdEncoding.EncodeToString(value)); err != nil { + panic(err) + } + + e := extractRelationshipsEnvs(r) + + c.Assert("mysql://mysql:xxx@dbread.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.6.0-MariaDB", DeepEquals, e["DBREAD_URL"]) + c.Assert("mysql://mysql:xxx@db.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.6.0-MariaDB", DeepEquals, e["DB_URL"]) +} + +func (s *RemoteSuite) TestNoIPsForDedicated(c *C) { + r := &Remote{} + value, err := os.ReadFile("testdata/dedicated/no_ips_for_dedicated.json") + if err != nil { + panic(err) + } + if err := os.Setenv("PLATFORM_RELATIONSHIPS", base64.StdEncoding.EncodeToString(value)); err != nil { + panic(err) + } + + rels := extractRelationshipsEnvs(r) + c.Assert(rels, DeepEquals, Envs{ + "DATABASE_DATABASE": "x_stg", + "DATABASE_DRIVER": "mysql", + "DATABASE_HOST": "127.0.0.1", + "DATABASE_NAME": "x_stg", + "DATABASE_PASSWORD": "x", + "DATABASE_PORT": "3306", + "DATABASE_SERVER": "mysql://127.0.0.1:3306", + "DATABASE_URL": "mysql://xstg:x@127.0.0.1:3306/x_stg?sslmode=disable&charset=utf8mb4", + "DATABASE_USER": "xstg", + "DATABASE_USERNAME": "xstg", + "RABBITMQ_VHOST": "x_stg", + "RABBITMQ_DSN": "amqp://x_stg:x@localhost:5672/x_stg", + "RABBITMQ_HOST": "localhost", + "RABBITMQ_MANAGEMENT_HOST": "localhost", + "RABBITMQ_MANAGEMENT_PASSWORD": "x", + "RABBITMQ_MANAGEMENT_PORT": "15672", + "RABBITMQ_MANAGEMENT_SCHEME": "http", + "RABBITMQ_MANAGEMENT_SERVER": "http://localhost:15672", + "RABBITMQ_MANAGEMENT_URL": "http://x_stg:x@localhost:15672", + "RABBITMQ_MANAGEMENT_USER": "x_stg", + "RABBITMQ_MANAGEMENT_USERNAME": "x_stg", + "RABBITMQ_PASSWORD": "x", + "RABBITMQ_PORT": "5672", + "RABBITMQ_SCHEME": "amqp", + "RABBITMQ_SERVER": "amqp://localhost:5672", + "RABBITMQ_URL": "amqp://x_stg:x@localhost:5672/x_stg", + "RABBITMQ_USER": "x_stg", + "RABBITMQ_USERNAME": "x_stg", + "REDISCACHE_HOST": "localhost", + "REDISCACHE_PORT": "6379", + "REDISCACHE_SCHEME": "redis", + "REDISCACHE_URL": "redis://localhost:6379", + }) } diff --git a/envs/testdata/dedicated/no_ips_for_dedicated.json b/envs/testdata/dedicated/no_ips_for_dedicated.json new file mode 100644 index 00000000..81f09371 --- /dev/null +++ b/envs/testdata/dedicated/no_ips_for_dedicated.json @@ -0,0 +1,33 @@ +{ + "database" : [ + { + "host" : "127.0.0.1", + "password" : "x", + "path" : "x_stg", + "port" : "3306", + "query" : { + "compression" : true, + "is_master" : true + }, + "scheme" : "mysql", + "username" : "xstg" + } + ], + "rabbitmq" : [ + { + "host" : "localhost", + "password" : "x", + "port" : "5672", + "scheme" : "amqp", + "username" : "x_stg", + "vhost" : "x_stg" + } + ], + "rediscache" : [ + { + "host" : "localhost", + "port" : "6379", + "scheme" : "redis" + } + ] +} diff --git a/envs/testdata/dedicated/relationships_with_read_replica.json b/envs/testdata/dedicated/relationships_with_read_replica.json new file mode 100644 index 00000000..36242d0e --- /dev/null +++ b/envs/testdata/dedicated/relationships_with_read_replica.json @@ -0,0 +1,46 @@ +{ + "db": [ + { + "username": "mysql", + "scheme": "mysql", + "service": "db", + "fragment": null, + "ip": "169.254.193.18", + "hostname": "x2.db.service.._.platform.sh", + "port": 3306, + "cluster": "zz-master-7rqtwti", + "host": "db.internal", + "rel": "mysql", + "path": "main", + "query": { + "is_master": true + }, + "password": "xxx", + "type": "mariadb:10.6", + "public": false, + "host_mapped": false + } + ], + "dbread": [ + { + "username": "mysql", + "scheme": "mysql", + "service": "db", + "fragment": null, + "ip": "169.254.150.110", + "hostname": "x2.db.service.._.platform.sh", + "port": 3306, + "cluster": "zz-master-7rqtwti", + "host": "dbread.internal", + "rel": "mysql-replica", + "path": "main", + "query": { + "is_master": false + }, + "password": "xxx", + "type": "mariadb:10.6", + "public": false, + "host_mapped": false + } + ] +} diff --git a/envs/testdata/doctrine-project/config/packages/doctrine.yaml b/envs/testdata/doctrine-project/config/packages/doctrine.yaml new file mode 100644 index 00000000..cb7fcf7e --- /dev/null +++ b/envs/testdata/doctrine-project/config/packages/doctrine.yaml @@ -0,0 +1,11 @@ +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + use_savepoints: true + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + server_version: '8.0.33' + driver: 'mysql' + + profiling_collect_backtrace: '%kernel.debug%' diff --git a/envs/testdata/upsun/.upsun/local/project.yaml b/envs/testdata/upsun/.upsun/local/project.yaml new file mode 100644 index 00000000..b65886e8 --- /dev/null +++ b/envs/testdata/upsun/.upsun/local/project.yaml @@ -0,0 +1,2 @@ +id: ism4mega7wpx6 +host: fr-3.platform.sh diff --git a/git/init.go b/git/init.go index a4ab30f8..ac312e38 100644 --- a/git/init.go +++ b/git/init.go @@ -21,11 +21,14 @@ package git import "bytes" -func Init(dir string, debug bool) (*bytes.Buffer, error) { +func Init(dir string, forceMainGitBranchName, debug bool) (*bytes.Buffer, error) { if content, err := doExecGit(dir, []string{"init"}, !debug); err != nil { return content, err } - return doExecGit(dir, []string{"checkout", "-b", "main"}, !debug) + if forceMainGitBranchName { + return doExecGit(dir, []string{"checkout", "-b", "main"}, !debug) + } + return nil, nil } func AddAndCommit(dir string, files []string, msg string, debug bool) (*bytes.Buffer, error) { diff --git a/git/remote.go b/git/remote.go index d319034b..13a33995 100644 --- a/git/remote.go +++ b/git/remote.go @@ -81,3 +81,14 @@ func GetUpstreamBranch(cwd string, remoteNames ...string) string { return "" } + +func GetRemoteURL(cwd, remote string) string { + args := []string{"config", "--get", fmt.Sprintf("remote.%s.url", remote)} + + out, err := execGitQuiet(cwd, args...) + if err != nil { + return "" + } + + return strings.Trim(out.String(), " \n") +} diff --git a/go.mod b/go.mod index 15088f05..ad27bb1e 100644 --- a/go.mod +++ b/go.mod @@ -1,88 +1,92 @@ module github.com/symfony-cli/symfony-cli +go 1.24.0 + require ( - github.com/compose-spec/compose-go v1.2.4 - github.com/docker/docker v20.10.14+incompatible - github.com/elazarl/goproxy v0.0.0-20220417044921-416226498f94 - github.com/fabpot/local-php-security-checker/v2 v2.0.3 + github.com/NYTimes/gziphandler v1.1.1 + github.com/blackfireio/osinfo v1.0.5 + github.com/compose-spec/compose-go v1.20.2 + github.com/docker/docker v28.0.0+incompatible + github.com/elazarl/goproxy v1.7.0 + github.com/fabpot/local-php-security-checker/v2 v2.1.3 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 - github.com/hashicorp/go-version v1.4.0 - github.com/hashicorp/golang-lru v0.5.4 - github.com/hpcloud/tail v1.0.0 - github.com/joho/godotenv v1.4.0 + github.com/hashicorp/go-version v1.7.0 + github.com/hashicorp/golang-lru/arc/v2 v2.0.7 + github.com/joho/godotenv v1.5.1 github.com/mitchellh/go-homedir v1.1.0 + github.com/nxadm/tail v1.4.11 github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/errors v0.9.1 - github.com/rs/xid v1.4.0 - github.com/rs/zerolog v1.26.1 + github.com/posener/complete v1.2.3 + github.com/rjeczalik/notify v0.9.3 + github.com/rs/xid v1.6.0 + github.com/rs/zerolog v1.33.0 + github.com/schollz/progressbar/v3 v3.18.0 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/soheilhy/cmux v0.1.5 - github.com/spf13/viper v1.11.0 github.com/stoicperlman/fls v0.0.0-20171222144224-f073b7a01081 - github.com/symfony-cli/cert v1.0.1 - github.com/symfony-cli/console v1.0.2 - github.com/symfony-cli/phpstore v1.0.5 - github.com/symfony-cli/terminal v1.0.3 - github.com/syncthing/notify v0.0.0-20210616190510-c6b7342338d2 - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + github.com/symfony-cli/cert v1.0.6 + github.com/symfony-cli/console v1.2.1 + github.com/symfony-cli/phpstore v1.0.12 + github.com/symfony-cli/terminal v1.0.7 + golang.org/x/sync v0.11.0 + golang.org/x/text v0.22.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 ) require ( - github.com/Microsoft/go-winio v0.5.2 // indirect - github.com/containerd/containerd v1.6.3 // indirect - github.com/distribution/distribution/v3 v3.0.0-20220427074907-edf5aa3c399f // indirect - github.com/docker/distribution v2.8.1+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect - github.com/docker/go-units v0.4.0 // indirect - github.com/ferhatelmas/levenshtein v0.0.0-20160518143259-a12aecc52d76 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/btree v1.0.1 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/imdario/mergo v0.3.12 // indirect - github.com/kr/pretty v0.3.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect - github.com/magiconair/properties v1.8.6 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.2 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect - github.com/rivo/uniseg v0.2.0 // indirect - github.com/rogpeppe/go-internal v1.8.1 // indirect - github.com/sirupsen/logrus v1.8.1 // indirect - github.com/spf13/afero v1.8.2 // indirect - github.com/spf13/cast v1.4.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect - golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect - golang.org/x/sys v0.0.0-20220429233432-b5fbb4746d32 // indirect - golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect - golang.org/x/text v0.3.7 // indirect - google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e // indirect - google.golang.org/grpc v1.46.0 // indirect - google.golang.org/protobuf v1.28.0 // indirect - gopkg.in/fsnotify.v1 v1.4.7 // indirect - gopkg.in/ini.v1 v1.66.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/time v0.10.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - howett.net/plist v1.0.0 // indirect - software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + howett.net/plist v1.0.1 // indirect + software.sslmate.com/src/go-pkcs12 v0.5.0 // indirect ) - -go 1.18 diff --git a/go.sum b/go.sum index 801ee202..a4697627 100644 --- a/go.sum +++ b/go.sum @@ -1,405 +1,165 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= -github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aws/aws-sdk-go v1.43.16/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/compose-spec/compose-go v1.2.4 h1:nzTFqM8+2J7Veao5Pq5U451thinv3U1wChIvcjX59/A= -github.com/compose-spec/compose-go v1.2.4/go.mod h1:pAy7Mikpeft4pxkFU565/DRHEbDfR84G6AQuiL+Hdg8= -github.com/containerd/containerd v1.6.3 h1:JfgUEIAH07xDWk6kqz0P3ArZt+KJ9YeihSC9uyFtSKg= -github.com/containerd/containerd v1.6.3/go.mod h1:gCVGrYRYFm2E8GmuUIbj/NGD7DLZQLzSJQazjVKDOig= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/blackfireio/osinfo v1.0.5 h1:6hlaWzfcpb87gRmznVf7wSdhysGqLRz9V/xuSdCEXrA= +github.com/blackfireio/osinfo v1.0.5/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= +github.com/compose-spec/compose-go v1.20.2 h1:u/yfZHn4EaHGdidrZycWpxXgFffjYULlTbRfJ51ykjQ= +github.com/compose-spec/compose-go v1.20.2/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/distribution/distribution/v3 v3.0.0-20220427074907-edf5aa3c399f h1:J0z4EUo8YMjvTSQm0OHRkV9Bdwm3gQZu4KlyjvfpxE8= -github.com/distribution/distribution/v3 v3.0.0-20220427074907-edf5aa3c399f/go.mod h1:qLi7jGj1b5TUaYTB3ekkHiocxHJi8+3CFhXM54MGKBs= -github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.14+incompatible h1:+T9/PRYWNDo5SZl5qS1r9Mo/0Q8AwxKKPtu9S1yxM0w= -github.com/docker/docker v20.10.14+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/elazarl/goproxy v0.0.0-20220417044921-416226498f94 h1:VIy7cdK7ufs7ctpTFkXJHm1uP3dJSnCGSPysEICB1so= -github.com/elazarl/goproxy v0.0.0-20220417044921-416226498f94/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= -github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fabpot/local-php-security-checker/v2 v2.0.3 h1:VSwl7Lz3M+8sSKkDbvEi0dtMPCMhHct8EeS7MPzetys= -github.com/fabpot/local-php-security-checker/v2 v2.0.3/go.mod h1:YOYs9Zbva+kkuQB8EjBj+lgdgf5+CkyT4xfc3mP1oLc= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/ferhatelmas/levenshtein v0.0.0-20160518143259-a12aecc52d76 h1:R2K7yLHPmwoTdmNuQ5Wfm0/evh/+QFdoI6EbW3OSMKw= -github.com/ferhatelmas/levenshtein v0.0.0-20160518143259-a12aecc52d76/go.mod h1:jr1MMO0KsDD+PQ57K7oJGk97TLYtbhLUOjCS577Ddm4= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM= +github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= +github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/fabpot/local-php-security-checker/v2 v2.1.3 h1:sL69IHlEvlmaOnyzfOhIAbrG1Ugp2IibM3f6JVxV+yk= +github.com/fabpot/local-php-security-checker/v2 v2.1.3/go.mod h1:t4Qk2u9Mj4ZM05X4cnwuwqrHGDKohweR8ox5rFBPBls= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= -github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8zEbfAAzRv52KQ= +github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.0 h1:P7Bq0SaI8nsexyay5UAyDo+ICWy5MQPgEZ5+l8JQTKo= -github.com/pelletier/go-toml/v2 v2.0.0/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= -github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= -github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= +github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= +github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= -github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= github.com/stoicperlman/fls v0.0.0-20171222144224-f073b7a01081 h1:vEf6LukDDCcMnRXnIMy5XRo/MR4+3lNTAhXxM+x0ZXI= github.com/stoicperlman/fls v0.0.0-20171222144224-f073b7a01081/go.mod h1:mXF6uSYLo1a2BZPcVHpP8RstMYoQnCaFF+dmIY726NY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/symfony-cli/cert v1.0.1 h1:ETYVBshgY+SaydBJmMkU0PaoDrWm6zsGNB/799ZQYCE= -github.com/symfony-cli/cert v1.0.1/go.mod h1:g4WrLT6EQsEPmA19xh5Jv9Jnpg5EvtFqArJf2AG2S+w= -github.com/symfony-cli/console v1.0.2 h1:u8KJm9jFbfzmN0y7fcfjjal3wWOLflNomu29PLgYQjQ= -github.com/symfony-cli/console v1.0.2/go.mod h1:z2dLSNdPW3rWdSxj8DlZocMtMYN5EF6OeIYjVioXVqE= -github.com/symfony-cli/phpstore v1.0.5 h1:e1J+FcztiSSAVuD4gwatPwMpeqQy3SGNdhd6Vtuncy8= -github.com/symfony-cli/phpstore v1.0.5/go.mod h1:Pug4pGst4b5DcGUwYz2DB1LjltohPgvE4OusDe1z2Xg= -github.com/symfony-cli/terminal v1.0.3 h1:0/Vc6uY1UaHBOkB5ft/OZHHy/zMOPutTmGXtnqyDkPQ= -github.com/symfony-cli/terminal v1.0.3/go.mod h1:+OxxnU05wyRHKYyQkTzTaCSSxmmIBcvAHLcQ099odj4= -github.com/syncthing/notify v0.0.0-20210616190510-c6b7342338d2 h1:F4snRP//nIuTTW9LYEzVH4HVwDG9T3M4t8y/2nqMbiY= -github.com/syncthing/notify v0.0.0-20210616190510-c6b7342338d2/go.mod h1:J0q59IWjLtpRIJulohwqEZvjzwOfTEPp8SVhDJl+y0Y= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/symfony-cli/cert v1.0.6 h1:FKdNRhKSxc+IcOkSRYvcOjr4jyZxGHiNS0xCN0uXZQI= +github.com/symfony-cli/cert v1.0.6/go.mod h1:7Lt0uwi9z6DYTwLQeKsdPrsTqvTZRTqdlVSDJJqKUVo= +github.com/symfony-cli/console v1.2.1 h1:j3ft4sWNXmFmmHACsXUZrAvZE52rIopg/FpZMkknZz4= +github.com/symfony-cli/console v1.2.1/go.mod h1:AB4ZxA593cyS/1NhwnDEUChIPaGuddFqooipam1vyS8= +github.com/symfony-cli/phpstore v1.0.12 h1:2mKJrDielSCW+7B+63w6HebmSBcB4qV7uuvNrIjLkoA= +github.com/symfony-cli/phpstore v1.0.12/go.mod h1:U29bdJBPs9p28PzLIRKfKfKkaiH0kacdyufl3eSB1d4= +github.com/symfony-cli/terminal v1.0.7 h1:57L9PUTE2cHfQtP8Ti8dyiiPEYlQ1NBIDpMJ3RPEGPc= +github.com/symfony-cli/terminal v1.0.7/go.mod h1:Etv22IyeGiMoIQPPj51hX31j7xuYl1njyuAFkrvybqU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -407,407 +167,97 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= -golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220429233432-b5fbb4746d32 h1:Js08h5hqB5xyWR789+QqueR6sDE8mk+YvpETZ+F6X9Y= -golang.org/x/sys v0.0.0-20220429233432-b5fbb4746d32/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= -golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= -google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e h1:gMjH4zLGs9m+dGzR7qHCHaXMOwsJHJKKkHtyXhtOrJk= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= -gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= -howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= -software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= +howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= +software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/inotify/inotify.go b/inotify/inotify.go index 68394c3b..6e30bb1b 100644 --- a/inotify/inotify.go +++ b/inotify/inotify.go @@ -19,7 +19,7 @@ package inotify -import "github.com/syncthing/notify" +import "github.com/rjeczalik/notify" // Create, Remove, Write and Rename are the only event values guaranteed to be // present on all platforms. diff --git a/inotify/inotify_posix.go b/inotify/inotify_posix.go index 76acef8a..62a9909c 100644 --- a/inotify/inotify_posix.go +++ b/inotify/inotify_posix.go @@ -22,7 +22,7 @@ package inotify -import "github.com/syncthing/notify" +import "github.com/rjeczalik/notify" func Watch(path string, c chan<- notify.EventInfo, events ...notify.Event) error { return simpleWatch(path, c, events...) diff --git a/inotify/inotify_windows.go b/inotify/inotify_windows.go index 6d68f673..09ec1f80 100644 --- a/inotify/inotify_windows.go +++ b/inotify/inotify_windows.go @@ -24,7 +24,7 @@ import ( "path/filepath" "github.com/pkg/errors" - "github.com/syncthing/notify" + "github.com/rjeczalik/notify" ) func Watch(path string, c chan<- notify.EventInfo, events ...notify.Event) error { diff --git a/installer/bash-installer b/installer/bash-installer new file mode 100755 index 00000000..18940615 --- /dev/null +++ b/installer/bash-installer @@ -0,0 +1,241 @@ +#!/usr/bin/env bash +# Copyright (c) 2021-present Fabien Potencier +# +# Symfony CLI installer: this file is part of Symfony CLI project. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +set -euo pipefail + +CLI_CONFIG_DIR=".symfony5" +CLI_EXECUTABLE="symfony" +CLI_TMP_NAME="$CLI_EXECUTABLE-"$(date +"%s") +CLI_NAME="Symfony CLI" +CLI_VERSION="${CLI_VERSION:-latest}" +CLI_DOWNLOAD_URL_LATEST_PATTERN="https://github.com/symfony-cli/symfony-cli/releases/latest/download/symfony-cli_~platform~.tar.gz" +CLI_DOWNLOAD_URL_VERSION_PATTERN="https://github.com/symfony-cli/symfony-cli/releases/download/v~version~/symfony-cli_~platform~.tar.gz" + +CLI_TMPDIR="${TMPDIR:-/tmp}" + +function output { + style_start="" + style_end="" + if [ "${2:-}" != "" ]; then + case $2 in + "success") + style_start="\033[0;32m" + style_end="\033[0m" + ;; + "error") + style_start="\033[31;31m" + style_end="\033[0m" + ;; + "info"|"warning") + style_start="\033[33m" + style_end="\033[39m" + ;; + "heading") + style_start="\033[1;33m" + style_end="\033[22;39m" + ;; + esac + fi + + builtin echo -e "${style_start}${1}${style_end}" +} + +output "${CLI_NAME} installer" "heading" +binary_dest="${HOME}/${CLI_CONFIG_DIR}/bin" +custom_dir="false" + +# Getops does not support long option names +while [[ $# -gt 0 ]]; do +case $1 in + --install-dir=*) + binary_dest="${1#*=}" + custom_dir="true" + shift # past argument=value + ;; + --install-dir) + binary_dest="${2:-}" + custom_dir="true" + shift # past argument + shift # past value + ;; + *) + output "Unknown option $1" "error" + output "Usage: ${0} [--install-dir=dir]" + exit 1 + ;; +esac +done + +output "\nSanity check" "heading" + +# Check that the version is valid +if [[ "$CLI_VERSION" =~ ^[0-9]+(\.[0-9]+)*$ || "$CLI_VERSION" == 'latest' ]]; then + output " [*] Version has valid format" "success" +else + output " [ ] ERROR: Version has invalid format." "error" + exit 1 +fi + +# Run environment checks. +output "\nEnvironment check" "heading" + +# Check that cURL or wget is installed. +downloader="" +if command -v curl >/dev/null 2>&1; then + downloader="curl" + output " [*] cURL is installed" "success" +elif command -v wget >/dev/null 2>&1; then + downloader="wget" + output " [*] wget is installed" "success" +else + output " [ ] ERROR: cURL or wget is required for installation." "error" + exit 1 +fi + +# Check that tar is installed. +if command -v tar >/dev/null 2>&1; then + output " [*] Tar is installed" "success" +else + output " [ ] ERROR: Tar is required for installation." "error" + exit 1 +fi + +# Check that Git is installed. +if command -v git >/dev/null 2>&1; then + output " [*] Git is installed" "success" +else + output " [ ] Warning: Git will be needed." "warning" +fi + +kernel=$(uname -s 2>/dev/null || /usr/bin/uname -s) +case ${kernel} in + "Linux"|"linux") + kernel="linux" + ;; + "Darwin"|"darwin") + kernel="darwin" + ;; + *) + output "OS '${kernel}' not supported" "error" + exit 1 + ;; +esac + +machine=$(uname -m 2>/dev/null || /usr/bin/uname -m) +case ${machine} in + arm|armv6*) + machine="armv6" + ;; + armv7*) + # ARMv6 is upwards compatible with ARMv7 + machine="armv6" + ;; + aarch64*|armv8*|arm64) + machine="arm64" + ;; + i[36]86) + machine="386" + if [ "darwin" = "${kernel}" ]; then + output " [ ] Your architecture (${machine}) is not supported anymore" "error" + exit 1 + fi + ;; + x86_64) + machine="amd64" + ;; + *) + output " [ ] Your architecture (${machine}) is not currently supported" "error" + exit 1 + ;; +esac + +output " [*] Your architecture (${machine}) is supported" "success" + +if [ "darwin" = "${kernel}" ]; then + machine="all" +fi + +platform="${kernel}_${machine}" + +# The necessary checks have passed. Start downloading the right version. +output "\nDownload" "heading" + +download_url="${CLI_DOWNLOAD_URL_LATEST_PATTERN}" +if [[ "$CLI_VERSION" != 'latest' ]]; then + download_url=${CLI_DOWNLOAD_URL_VERSION_PATTERN/~version~/${CLI_VERSION}} +fi + +download_url=${download_url/~platform~/${platform}} +output " Downloading ${download_url}..."; +case $downloader in + "curl") + curl --fail --location "${download_url}" > "${CLI_TMPDIR}/${CLI_TMP_NAME}.tar.gz" + ;; + "wget") + wget -q --show-progress "${download_url}" -O "${CLI_TMPDIR}/${CLI_TMP_NAME}.tar.gz" + ;; +esac + +# shellcheck disable=SC2181 +if [ $? != 0 ]; then + output " The download failed." "error" + exit 1 +fi + +output " Uncompress binary..." +tar -xz --directory "${CLI_TMPDIR}" -f "${CLI_TMPDIR}/${CLI_TMP_NAME}.tar.gz" +rm "${CLI_TMPDIR}/${CLI_TMP_NAME}.tar.gz" + +if [ ! -d "${binary_dest}" ]; then + if ! mkdir -p "${binary_dest}"; then + binary_dest="." + fi +fi + +if [ "${custom_dir}" == "true" ]; then + output " Installing the binary into ${binary_dest} ..." +else + output " Installing the binary into your home directory..." +fi + +if mv "${CLI_TMPDIR}/${CLI_EXECUTABLE}" "${binary_dest}/${CLI_EXECUTABLE}"; then + output " The binary was saved to: ${binary_dest}/${CLI_EXECUTABLE}" +else + output " Failed to move the binary to ${binary_dest}." "error" + rm "${CLI_TMPDIR}/${CLI_EXECUTABLE}" + exit 1 +fi + +#output " Installing the shell auto-completion..." +#"${binary_dest}/${CLI_EXECUTABLE}" self:shell-setup --silent +#if [ $? != 0 ]; then +# output " Failed to install the shell auto-completion." "warning" +#fi + +output "\nThe ${CLI_NAME} was installed successfully!" "success" + +if [ "${custom_dir}" == "false" ]; then + output "\nUse it as a local file:" "info" + output " ${binary_dest}/${CLI_EXECUTABLE}" + output "\nOr add the following line to your shell configuration file:" "info" + output " export PATH=\"\$HOME/${CLI_CONFIG_DIR}/bin:\$PATH\"" + output "\nOr install it globally on your system:" "info" + output " mv ${binary_dest}/${CLI_EXECUTABLE} /usr/local/bin/${CLI_EXECUTABLE}" + output "\nIf moving the file does not work, you might have to prefix the command with sudo." + output "\nThen start a new shell and run '${CLI_EXECUTABLE}'" "info" +fi diff --git a/local/fcgi_client/fcgiclient.go b/local/fcgi_client/fcgiclient.go index d3d68e42..77ff9da6 100644 --- a/local/fcgi_client/fcgiclient.go +++ b/local/fcgi_client/fcgiclient.go @@ -10,7 +10,6 @@ import ( "bytes" "encoding/binary" "io" - "io/ioutil" "mime/multipart" "net" "net/http" @@ -81,7 +80,7 @@ type header struct { Reserved uint8 } -// for padding so we don't have to allocate all the time +// for padding, so we don't have to allocate all the time // not synchronized because we don't care what the contents are var pad [maxPad]byte @@ -322,7 +321,7 @@ func (w *streamReader) Read(p []byte) (n int, err error) { return } -// Do made the request and returns a io.Reader that translates the data read +// Do makes the request and returns an io.Reader that translates the data read // from fcgi responder out of fcgi packet before returning it. func (f *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) { err = f.writeBeginRequest(uint16(FCGI_RESPONDER), 0) @@ -345,7 +344,7 @@ func (f *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err er return } -// Request returns a HTTP Response with Header and Body +// Request returns an HTTP Response with Header and Body // from fcgi responder func (f *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) { r, err := f.Do(p, req) @@ -376,9 +375,9 @@ func (f *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res } if chunked(resp.TransferEncoding) { - resp.Body = ioutil.NopCloser(httputil.NewChunkedReader(rb)) + resp.Body = io.NopCloser(httputil.NewChunkedReader(rb)) } else { - resp.Body = ioutil.NopCloser(rb) + resp.Body = io.NopCloser(rb) } return } diff --git a/local/http/cors.go b/local/http/cors.go new file mode 100644 index 00000000..56abf2bd --- /dev/null +++ b/local/http/cors.go @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package http + +import ( + "net/http" + + "github.com/rs/zerolog" +) + +func corsWrapper(h http.Handler, logger zerolog.Logger) http.Handler { + var corsHeaders = []string{"Access-Control-Allow-Origin", "Access-Control-Allow-Methods", "Access-Control-Allow-Headers"} + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for _, corsHeader := range corsHeaders { + w.Header().Set(corsHeader, "*") + } + + h.ServeHTTP(w, r) + + for _, corsHeader := range corsHeaders { + if headers, exists := w.Header()[corsHeader]; !exists || len(headers) < 2 { + continue + } + + logger.Warn().Msgf(`Multiple entries detected for header "%s". Only one should be set: you should enable CORS handling in the CLI only if the application does not handle them.`, corsHeader) + } + }) +} diff --git a/local/http/http.go b/local/http/http.go index f70f2eab..650ed678 100644 --- a/local/http/http.go +++ b/local/http/http.go @@ -22,6 +22,7 @@ package http import ( "crypto/tls" "fmt" + "io" "net" "net/http" "os" @@ -30,6 +31,7 @@ import ( "strconv" "strings" + "github.com/NYTimes/gziphandler" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/soheilhy/cmux" @@ -47,10 +49,14 @@ type Server struct { Callback ServerCallback Port int PreferredPort int + ListenIp string PKCS12 string AllowHTTP bool Logger zerolog.Logger Appversion string + UseGzip bool + TlsKeyLogFile string + AllowCORS bool httpserver *http.Server httpsserver *http.Server @@ -58,16 +64,47 @@ type Server struct { serverPort string } +var gzipContentTypes = []string{ + "text/html", + "text/plain", + "text/csv", + "text/javascript", + "text/css", + "text/xml", + "application/json", + "application/javascript", + "application/vnd.api+json", + "application/atom+xml", + "application/rss+xml", + "image/svg+xml", +} + // Start starts the server func (s *Server) Start(errChan chan error) (int, error) { - ln, port, err := process.CreateListener(s.Port, s.PreferredPort) + ln, port, err := process.CreateListener(s.ListenIp, s.Port, s.PreferredPort) if err != nil { return port, errors.WithStack(err) } s.serverPort = strconv.Itoa(port) + var proxyHandler http.Handler + + proxyHandler = http.HandlerFunc(s.ProxyHandler) + + if s.UseGzip { + gzipWrapper, err := gziphandler.GzipHandlerWithOpts(gziphandler.ContentTypes(gzipContentTypes)) + if err != nil { + return port, errors.WithStack(err) + } + proxyHandler = gzipWrapper(proxyHandler) + } + + if s.AllowCORS { + proxyHandler = corsWrapper(proxyHandler, s.Logger) + } + s.httpserver = &http.Server{ - Handler: http.HandlerFunc(s.ProxyHandler), + Handler: proxyHandler, } if s.PKCS12 == "" { go func() { @@ -82,18 +119,29 @@ func (s *Server) Start(errChan chan error) (int, error) { return port, errors.WithStack(err) } + var keyLogWriter io.Writer + if s.TlsKeyLogFile != "" { + w, err := os.OpenFile(s.TlsKeyLogFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) + if err != nil { + return port, errors.WithStack(err) + } + + keyLogWriter = w + } + s.httpsserver = &http.Server{ - Handler: http.HandlerFunc(s.ProxyHandler), + Handler: proxyHandler, TLSConfig: &tls.Config{ PreferServerCipherSuites: true, CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, Certificates: []tls.Certificate{cert}, NextProtos: []string{"h2", "http/1.1"}, + KeyLogWriter: keyLogWriter, }, } m := cmux.New(ln) - httpl := m.Match(cmux.HTTP1Fast()) + httpl := m.Match(cmux.HTTP1Fast(http.MethodPatch)) tlsl := m.Match(cmux.Any()) if !s.AllowHTTP { @@ -161,7 +209,7 @@ func (s *Server) ProxyHandler(w http.ResponseWriter, r *http.Request) { } else if status >= 400 { l = s.Logger.Warn() } - l = l.Str("ip", ip).Int("status", status).Str("method", r.Method).Str("scheme", "https").Str("host", "127.0.0.1:8004") + l = l.Str("ip", ip).Int("status", status).Str("method", r.Method).Str("scheme", "https").Str("host", r.Host) if len(resources) > 0 { l.Strs("preloaded_resources", resources) } @@ -190,9 +238,9 @@ func (s *Server) Handler(w http.ResponseWriter, r *http.Request) { } env := map[string]string{ "SERVER_PORT": s.serverPort, - "SERVER_NAME": r.Host, + "SERVER_NAME": strings.Split(r.Host, ":")[0], "SERVER_PROTOCOL": r.Proto, - "SERVER_SOFTWARE": fmt.Sprintf("Symfony Local Server %s", s.Appversion), + "SERVER_SOFTWARE": fmt.Sprintf("symfony-cli/%s", s.Appversion), } env["X_FORWARDED_PORT"] = r.Header.Get("X-Forwarded-Port") if env["X_FORWARDED_PORT"] == "" { diff --git a/local/http/push.go b/local/http/push.go index b28d01e6..543670b3 100644 --- a/local/http/push.go +++ b/local/http/push.go @@ -56,6 +56,9 @@ func (s *Server) servePreloadLinks(w http.ResponseWriter, r *http.Request) ([]st if _, exists := resource.params["nopush"]; exists { continue } + if rel, exists := resource.params["rel"]; exists && rel != "preload" { + continue + } if isRemoteResource(resource.uri) { continue } diff --git a/local/logs/tailer.go b/local/logs/tailer.go index 365a7a53..628697b4 100644 --- a/local/logs/tailer.go +++ b/local/logs/tailer.go @@ -28,14 +28,14 @@ import ( "strings" "sync" - "github.com/hpcloud/tail" + "github.com/nxadm/tail" "github.com/pkg/errors" + realinotify "github.com/rjeczalik/notify" "github.com/stoicperlman/fls" "github.com/symfony-cli/symfony-cli/humanlog" "github.com/symfony-cli/symfony-cli/inotify" "github.com/symfony-cli/symfony-cli/local/pid" "github.com/symfony-cli/terminal" - realinotify "github.com/syncthing/notify" ) type namedLine struct { @@ -253,7 +253,7 @@ func tailFile(filename string, follow bool, nblines int64) (*tail.Tail, error) { return tail.TailFile(filename, tail.Config{ Location: &tail.SeekInfo{ Offset: pos, - Whence: os.SEEK_SET, + Whence: io.SeekStart, }, ReOpen: follow, Follow: follow, diff --git a/local/php/cgi.go b/local/php/cgi.go new file mode 100644 index 00000000..e1eb3217 --- /dev/null +++ b/local/php/cgi.go @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package php + +import ( + "bytes" + "io" + "net/http" + "strconv" + "time" + + "github.com/pkg/errors" + fcgiclient "github.com/symfony-cli/symfony-cli/local/fcgi_client" +) + +type cgiTransport struct{} + +func (p *cgiTransport) RoundTrip(req *http.Request) (*http.Response, error) { + env := req.Context().Value(environmentContextKey).(map[string]string) + + // as the process might have been just created, it might not be ready yet + var fcgi *fcgiclient.FCGIClient + var err error + max := 10 + i := 0 + for { + if fcgi, err = fcgiclient.Dial("tcp", "127.0.0.1:"+req.URL.Port()); err == nil { + break + } + i++ + if i > max { + return nil, errors.Wrapf(err, "unable to connect to the PHP FastCGI process") + } + time.Sleep(time.Millisecond * 50) + } + + // The CGI spec doesn't allow chunked requests. Go is already assembling the + // chunks from the request to a usable Reader (see net/http.readTransfer and + // net/http/internal.NewChunkedReader), so the only thing we have to + // do to is get the content length and add it to the header but to do so we + // have to read and buffer the body content. + if len(req.TransferEncoding) > 0 && req.TransferEncoding[0] == "chunked" { + bodyBuffer := &bytes.Buffer{} + bodyBytes, err := io.Copy(bodyBuffer, req.Body) + if err != nil { + return nil, err + } + + req.Body = io.NopCloser(bodyBuffer) + req.TransferEncoding = nil + env["CONTENT_LENGTH"] = strconv.FormatInt(bodyBytes, 10) + env["HTTP_CONTENT_LENGTH"] = env["CONTENT_LENGTH"] + } + + // fetching the response from the fastcgi backend, and check for errors + resp, err := fcgi.Request(env, req.Body) + if err != nil { + return nil, errors.Wrapf(err, "unable to fetch the response from the backend") + } + resp.Body = cgiBodyReadCloser{resp.Body, fcgi} + resp.Request = req + + return resp, nil +} + +// cgiBodyReadCloser is responsible for postponing the CGI connection +// termination when the client finished reading the response. This effectively +// allows to "stream" the CGI response from the server to the client by removing +// the requirement for an in-between buffer. +type cgiBodyReadCloser struct { + io.Reader + *fcgiclient.FCGIClient +} + +func (f cgiBodyReadCloser) Close() error { + f.FCGIClient.Close() + return nil +} diff --git a/local/php/composer.go b/local/php/composer.go index a53160b1..a3b5de6e 100644 --- a/local/php/composer.go +++ b/local/php/composer.go @@ -26,7 +26,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "os" "path/filepath" @@ -57,6 +56,9 @@ func (c ComposerResult) ExitCode() int { } func Composer(dir string, args, env []string, stdout, stderr, logger io.Writer, debugLogger zerolog.Logger) ComposerResult { + if os.Getenv("COMPOSER_MEMORY_LIMIT") == "" { + env = append(env, "COMPOSER_MEMORY_LIMIT=-1") + } e := &Executor{ Dir: dir, BinName: "php", @@ -70,46 +72,41 @@ func Composer(dir string, args, env []string, stdout, stderr, logger io.Writer, if composerVersion() == 2 { composerBin = "composer2" } - path, err := e.findComposer(composerBin) - if err != nil || !isComposerPHPScript(path) { - fmt.Fprintln(logger, " WARNING: Unable to find Composer, downloading one. It is recommended to install Composer yourself at https://getcomposer.org/download/") + + if composerPath := os.Getenv("SYMFONY_COMPOSER_PATH"); composerPath != "" { + debugLogger.Debug().Str("SYMFONY_COMPOSER_PATH", composerPath).Msg("SYMFONY_COMPOSER_PATH has been defined. User is taking control over Composer detection and execution.") + e.Args = append([]string{composerPath}, args...) + } else if path, err := e.findComposer(composerBin); err == nil && isPHPScript(path) { + e.Args = append([]string{"php", path}, args...) + } else { + reason := "No Composer installation found." + if path != "" { + reason = fmt.Sprintf("Detected Composer file (%s) is not a valid PHAR or PHP script.", path) + } + fmt.Fprintln(logger, " WARNING:", reason) + fmt.Fprintln(logger, " Downloading Composer for you, but it is recommended to install Composer yourself, instructions available at https://getcomposer.org/download/") // we don't store it under bin/ to avoid it being found by findComposer as we want to only use it as a fallback binDir := filepath.Join(util.GetHomeDir(), "composer") - if path, err = downloadComposer(binDir); err != nil { + if path, err = downloadComposer(binDir, debugLogger); err != nil { return ComposerResult{ code: 1, error: errors.Wrap(err, "unable to find composer, get it at https://getcomposer.org/download/"), } } + e.Args = append([]string{"php", path}, args...) + fmt.Fprintf(logger, " (running %s)\n\n", e.CommandLine()) } - e.Args = append([]string{"php", path}, args...) - fmt.Fprintf(logger, " (running %s %s)\n\n", path, strings.TrimSpace(strings.Join(args, " "))) ret := e.Execute(false) if ret != 0 { return ComposerResult{ code: ret, - error: errors.Errorf("unable to run %s %s", path, strings.Join(args, " ")), + error: errors.Errorf("unable to run %s", e.CommandLine()), } } return ComposerResult{} } -// isComposerPHPScript checks that the composer file is indeed a phar/PHP script (not a .bat file) -func isComposerPHPScript(path string) bool { - file, err := os.Open(path) - if err != nil { - return false - } - defer file.Close() - magicPrefix := []byte("#!/usr/bin/env php") - byteSlice := make([]byte, len(magicPrefix)) - if _, err := file.Read(byteSlice); err != nil { - return false - } - return bytes.Equal(byteSlice, magicPrefix) -} - func composerVersion() int { var lock struct { Version string `json:"plugin-api-version"` @@ -118,7 +115,7 @@ func composerVersion() int { if err != nil { return DefaultComposerVersion } - contents, err := ioutil.ReadFile(filepath.Join(cwd, "composer.lock")) + contents, err := os.ReadFile(filepath.Join(cwd, "composer.lock")) if err != nil { return DefaultComposerVersion } @@ -131,26 +128,21 @@ func composerVersion() int { return DefaultComposerVersion } -func findComposer(extraBin string) (string, error) { - // Special Support for NixOS. It needs to run before the PATH detection - // because NixOS adds a shell wrapper that we can't run via PHP. - for _, path := range strings.Split(os.Getenv("buildInputs"), " ") { - nixPharPath := filepath.Join(path, "libexec/composer/composer.phar") - d, err := os.Stat(nixPharPath) - if err != nil { - continue - } - if m := d.Mode(); !m.IsDir() { - // Yep! - return nixPharPath, nil - } +func findComposer(extraBin string, logger zerolog.Logger) (string, error) { + // Special support for OS specific things. They need to run before the + // PATH detection because most of them adds shell wrappers that we + // can't run via PHP. + if pharPath := findComposerSystemSpecific(); pharPath != "" { + return pharPath, nil } for _, file := range []string{extraBin, "composer", "composer.phar"} { + logger.Debug().Str("source", "Composer").Msgf(`Looking for Composer in the PATH as "%s"`, file) if pharPath, _ := LookPath(file); pharPath != "" { // On Windows, we don't want the .bat, but the real composer phar/PHP file if strings.HasSuffix(pharPath, ".bat") { pharPath = pharPath[:len(pharPath)-4] + ".phar" } + logger.Debug().Str("source", "Composer").Msgf(`Found potential Composer as "%s"`, pharPath) return pharPath, nil } } @@ -158,7 +150,7 @@ func findComposer(extraBin string) (string, error) { return "", os.ErrNotExist } -func downloadComposer(dir string) (string, error) { +func downloadComposer(dir string, debugLogger zerolog.Logger) (string, error) { if err := os.MkdirAll(dir, 0755); err != nil { return "", err } @@ -184,7 +176,7 @@ func downloadComposer(dir string) (string, error) { return "", errors.New("signature was wrong when downloading Composer; please try again") } setupPath := filepath.Join(dir, "composer-setup.php") - ioutil.WriteFile(setupPath, installer, 0666) + os.WriteFile(setupPath, installer, 0666) var stdout bytes.Buffer e := &Executor{ @@ -194,6 +186,7 @@ func downloadComposer(dir string) (string, error) { SkipNbArgs: 1, Stdout: &stdout, Stderr: &stdout, + Logger: debugLogger, } ret := e.Execute(false) if ret == 1 { @@ -215,7 +208,7 @@ func downloadComposerInstaller() ([]byte, error) { return nil, err } defer resp.Body.Close() - return ioutil.ReadAll(resp.Body) + return io.ReadAll(resp.Body) } func downloadComposerInstallerSignature() ([]byte, error) { @@ -224,5 +217,5 @@ func downloadComposerInstallerSignature() ([]byte, error) { return nil, err } defer resp.Body.Close() - return ioutil.ReadAll(resp.Body) + return io.ReadAll(resp.Body) } diff --git a/local/php/composer_unix.go b/local/php/composer_unix.go new file mode 100644 index 00000000..54d0fc1a --- /dev/null +++ b/local/php/composer_unix.go @@ -0,0 +1,46 @@ +//go:build !windows +// +build !windows + +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package php + +import ( + "os" + "path/filepath" + "strings" +) + +func findComposerSystemSpecific() string { + // Special Support for NixOS + for _, path := range strings.Split(os.Getenv("buildInputs"), " ") { + nixPharPath := filepath.Join(path, "libexec/composer/composer.phar") + d, err := os.Stat(nixPharPath) + if err != nil { + continue + } + if m := d.Mode(); !m.IsDir() { + // Yep! + return nixPharPath + } + } + + return "" +} diff --git a/local/php/composer_windows.go b/local/php/composer_windows.go new file mode 100644 index 00000000..2b496d87 --- /dev/null +++ b/local/php/composer_windows.go @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package php + +import ( + "os" + "os/exec" + "path/filepath" + + "github.com/mitchellh/go-homedir" +) + +func findComposerSystemSpecific() string { + // Special Support for Scoop + scoopPaths := []string{} + + if scoopPath, err := exec.LookPath("scoop"); err == nil { + scoopPaths = append(scoopPaths, filepath.Dir(filepath.Dir(scoopPath))) + } + + if scoopPath := os.Getenv("SCOOP"); scoopPath == "" { + if homedir, err := homedir.Dir(); err != nil { + scoopPaths = append(scoopPaths, filepath.Join(homedir, "scoop")) + } + } + + if scoopGlobalPath := os.Getenv("SCOOP_GLOBAL"); scoopGlobalPath == "" { + if programData := os.Getenv("PROGRAMDATA"); programData != "" { + scoopPaths = append(scoopPaths, filepath.Join(programData, "scoop")) + } + } + + for _, path := range scoopPaths { + pharPath := filepath.Join(path, "apps", "composer", "current", "composer.phar") + d, err := os.Stat(pharPath) + if err != nil { + continue + } + if m := d.Mode(); !m.IsDir() { + // Yep! + return pharPath + } + } + + return "" +} diff --git a/local/php/context.go b/local/php/context.go new file mode 100644 index 00000000..b48fd1a5 --- /dev/null +++ b/local/php/context.go @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package php + +type phpServerContextKey string + +const ( + environmentContextKey phpServerContextKey = "env" + responseWriterContextKey phpServerContextKey = "rw" +) diff --git a/local/php/envs.go b/local/php/envs.go index a16b4e76..1465c552 100644 --- a/local/php/envs.go +++ b/local/php/envs.go @@ -23,6 +23,7 @@ import ( "net" "net/http" "os" + "path" "path/filepath" "strings" @@ -38,7 +39,7 @@ func (p *Server) generateEnv(req *http.Request) map[string]string { pathInfo := req.URL.Path if pos := strings.Index(strings.ToLower(pathInfo), ".php"); pos != -1 { - file := pathInfo[:pos+4] + file := path.Clean(pathInfo[:pos+4]) if _, err := os.Stat(filepath.Join(p.documentRoot, file)); err == nil { scriptName = file pathInfo = pathInfo[pos+4:] diff --git a/local/php/executor.go b/local/php/executor.go index 1e472711..740f8476 100644 --- a/local/php/executor.go +++ b/local/php/executor.go @@ -29,6 +29,7 @@ import ( "runtime" "strings" "syscall" + "time" "github.com/pkg/errors" "github.com/rs/xid" @@ -73,6 +74,10 @@ func GetBinaryNames() []string { return []string{"php", "pecl", "pear", "php-fpm", "php-cgi", "php-config", "phpdbg", "phpize"} } +func (e Executor) CommandLine() string { + return strings.TrimSpace(strings.Join(e.Args, " ")) +} + func (e *Executor) lookupPHP(cliDir string, forceReload bool) (*phpstore.Version, string, bool, error) { phpStore := phpstore.New(cliDir, forceReload, nil) v, source, warning, err := phpStore.BestVersionForDir(e.scriptDir) @@ -136,7 +141,38 @@ func (e *Executor) lookupPHP(cliDir string, forceReload bool) (*phpstore.Version return v, path, phpiniArgs, nil } -// Config determines the right version of PHP depending on the configuration (+ its configuration) +// DetectScriptDir detects the script dir based on the current configuration +func (e *Executor) DetectScriptDir() (string, error) { + if e.scriptDir != "" { + return e.scriptDir, nil + } + + if e.SkipNbArgs == 0 { + e.SkipNbArgs = 1 + } + + if e.SkipNbArgs < 0 { + wd, err := os.Getwd() + if err != nil { + return "", errors.WithStack(err) + } + e.scriptDir = wd + } else { + if len(e.Args) < 1 { + return "", errors.New("args cannot be empty") + } + + e.scriptDir = detectScriptDir(e.Args[e.SkipNbArgs:]) + } + + return e.scriptDir, nil +} + +// Config determines the right version of PHP depending on the configuration +// (+ its configuration). It also creates some symlinks to ease the integration +// with underlying tools that could try to run PHP. This is the responsibility +// of the caller to clean those temporary files. One can call +// CleanupTemporaryDirectories to do so. func (e *Executor) Config(loadDotEnv bool) error { // reset environment e.environ = make([]string, 0) @@ -144,19 +180,9 @@ func (e *Executor) Config(loadDotEnv bool) error { if len(e.Args) < 1 { return errors.New("args cannot be empty") } - if e.scriptDir == "" { - if e.SkipNbArgs == 0 { - e.SkipNbArgs = 1 - } - if e.SkipNbArgs < 0 { - wd, err := os.Getwd() - if err != nil { - return err - } - e.scriptDir = wd - } else { - e.scriptDir = detectScriptDir(e.Args[e.SkipNbArgs:]) - } + + if _, err := e.DetectScriptDir(); err != nil { + return err } vars := make(map[string]string) @@ -203,8 +229,10 @@ func (e *Executor) Config(loadDotEnv bool) error { // prepending the PHP directory in the PATH does not work well if the PHP binary is not named "php" (like php7.3 for instance) // in that case, we create a temp directory with a symlink // we also link php-config for pecl to pick up the right one (it is always looks for something called php-config) - phpDir := filepath.Join(cliDir, "tmp", xid.New().String(), "bin") - e.tempDir = phpDir + if e.tempDir == "" { + e.tempDir = filepath.Join(cliDir, "tmp", xid.New().String()) + } + phpDir := filepath.Join(e.tempDir, "bin") if err := os.MkdirAll(phpDir, 0755); err != nil { return err } @@ -260,31 +288,153 @@ func (e *Executor) Config(loadDotEnv bool) error { } } - // args[0] MUST be the same as path - // but as we change the path, we should update args[0] accordingly - e.Args[0] = path + if IsBinaryName(e.Args[0]) { + // args[0] MUST be the same as path + // but as we change the path, we should update args[0] accordingly + e.Args[0] = path + } return err } +func (e *Executor) CleanupTemporaryDirectories() { + backgroundCleanup := make(chan bool, 1) + go cleanupStaleTemporaryDirectories(e.Logger, backgroundCleanup) + + if e.iniDir != "" { + os.RemoveAll(e.iniDir) + } + if e.tempDir != "" { + os.RemoveAll(e.tempDir) + } + + // give some room to the background clean up job to do its work + select { + case <-backgroundCleanup: + case <-time.After(100 * time.Millisecond): + e.Logger.Debug().Msg("Allocated time for temporary directories to be cleaned up is over, it will resume later on") + } +} + +// The Symfony CLI used to leak temporary directories until v5.10.8. The bug is +// fixed but because directories names are random they are not going to be +// reused and thus are not going to be cleaned up. And because they might be +// in-use by running servers we can't simply delete the parent directory. This +// is why we make our best to find the oldest directories and remove then, +// cleaning the directory little by little. +func cleanupStaleTemporaryDirectories(mainLogger zerolog.Logger, doneCh chan<- bool) { + defer func() { + doneCh <- true + }() + parentDirectory := filepath.Join(util.GetHomeDir(), "tmp") + mainLogger = mainLogger.With().Str("dir", parentDirectory).Logger() + + if len(parentDirectory) < 6 { + mainLogger.Warn().Msg("temporary dir path looks too short") + return + } + + mainLogger.Debug().Msg("Starting temporary directory cleanup...") + dir, err := os.Open(parentDirectory) + if err != nil { + mainLogger.Warn().Err(err).Msg("Failed to open temporary directory") + return + } + defer dir.Close() + + // the duration after which we consider temporary directories as + // stale and can be removed + cutoff := time.Now().Add(-7 * 24 * time.Hour) + + for { + // we might have a lof of entries so we need to work in batches + entries, err := dir.Readdirnames(30) + if err == io.EOF { + mainLogger.Debug().Msg("Cleaning is done...") + return + } + if err != nil { + mainLogger.Warn().Err(err).Msg("Failed to read entries") + return + } + + for _, entry := range entries { + logger := mainLogger.With().Str("entry", entry).Logger() + + // we generate temporary directory names with + // `xid.New().String()` which is always 20 char long + if len(entry) != 20 { + logger.Debug().Msg("found an entry that is not from us") + continue + } else if _, err := xid.FromString(entry); err != nil { + logger.Debug().Err(err).Msg("found an entry that is not from us") + continue + } + + entryPath := filepath.Join(parentDirectory, entry) + file, err := os.Open(entryPath) + if err != nil { + logger.Warn().Err(err).Msg("failed to read entry") + continue + } else if fi, err := file.Stat(); err != nil { + logger.Warn().Err(err).Msg("failed to read entry") + continue + } else if !fi.IsDir() { + logger.Warn().Err(err).Msg("entry is not a directory") + continue + } else if fi.ModTime().After(cutoff) { + logger.Debug().Any("cutoff", cutoff).Msg("entry is more recent than cutoff, keeping it for now") + continue + } + + logger.Debug().Str("entry", entry).Msg("entry matches the criterias, removing it") + if err := os.RemoveAll(entryPath); err != nil { + logger.Warn().Err(err).Msg("failed to remove entry") + } + } + } +} + // Find composer depending on the configuration func (e *Executor) findComposer(extraBin string) (string, error) { - if e.Config(false) == nil { + if scriptDir, err := e.DetectScriptDir(); err == nil { for _, file := range []string{extraBin, "composer.phar", "composer"} { - path := filepath.Join(e.scriptDir, file) + path := filepath.Join(scriptDir, file) + e.Logger.Debug().Str("source", "Composer").Msgf(`Looking for Composer under "%s"`, path) d, err := os.Stat(path) if err != nil { continue } if m := d.Mode(); !m.IsDir() { // Yep! + e.Logger.Debug().Str("source", "Composer").Msgf(`Found potential Composer as "%s"`, path) return path, nil } } } // fallback to default composer detection - return findComposer(extraBin) + return findComposer(extraBin, e.Logger) +} + +// findPie locates the PIE binary depending on the configuration +func (e *Executor) findPie() (string, error) { + if scriptDir, err := e.DetectScriptDir(); err == nil { + for _, file := range []string{"pie.phar", "pie"} { + path := filepath.Join(scriptDir, file) + e.Logger.Debug().Str("source", "PIE").Msgf(`Looking for PIE under "%s"`, path) + d, err := os.Stat(path) + if err != nil { + continue + } + if m := d.Mode(); !m.IsDir() { + e.Logger.Debug().Str("source", "PIE").Msgf(`Found potential PIE as "%s"`, path) + return path, nil + } + } + } + + return findPie(e.Logger) } // Execute executes the right version of PHP depending on the configuration @@ -293,14 +443,7 @@ func (e *Executor) Execute(loadDotEnv bool) int { fmt.Fprintln(os.Stderr, err) return 1 } - defer func() { - if e.iniDir != "" { - os.RemoveAll(e.iniDir) - } - if e.tempDir != "" { - os.RemoveAll(e.tempDir) - } - }() + defer e.CleanupTemporaryDirectories() cmd := execCommand(e.Args[0], e.Args[1:]...) environ := append(os.Environ(), e.environ...) gpathname := "PATH" @@ -340,7 +483,7 @@ func (e *Executor) Execute(loadDotEnv bool) int { close(waitCh) }() - sigChan := make(chan os.Signal) + sigChan := make(chan os.Signal, 1) signal.Notify(sigChan) defer signal.Stop(sigChan) diff --git a/local/php/executor_test.go b/local/php/executor_test.go index 52216ce7..2608820c 100644 --- a/local/php/executor_test.go +++ b/local/php/executor_test.go @@ -23,7 +23,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -100,11 +99,12 @@ func TestHelperProcess(t *testing.T) { for _, v := range os.Environ() { fmt.Println(v) } + os.Exit(0) case "exit-code": code, _ := strconv.Atoi(os.Args[4]) os.Exit(code) } - os.Exit(0) + os.Exit(1) } func (s *ExecutorSuite) TestNotEnoughArgs(c *C) { @@ -113,6 +113,14 @@ func (s *ExecutorSuite) TestNotEnoughArgs(c *C) { c.Assert((&Executor{BinName: "php"}).Execute(true), Equals, 1) } +func (s *ExecutorSuite) TestCommandLineFormatting(c *C) { + c.Assert((&Executor{}).CommandLine(), Equals, "") + + c.Assert((&Executor{Args: []string{"php"}}).CommandLine(), Equals, "php") + + c.Assert((&Executor{Args: []string{"php", "-dmemory_limit=-1", "/path/to/composer.phar"}}).CommandLine(), Equals, "php -dmemory_limit=-1 /path/to/composer.phar") +} + func (s *ExecutorSuite) TestForwardExitCode(c *C) { defer restoreExecCommand() fakeExecCommand("exit-code", "5") @@ -132,6 +140,63 @@ func (s *ExecutorSuite) TestForwardExitCode(c *C) { c.Assert((&Executor{BinName: "php", Args: []string{"php"}}).Execute(true), Equals, 5) } +func (s *ExecutorSuite) TestExecutorRunsPHP(c *C) { + defer restoreExecCommand() + execCommand = func(name string, arg ...string) *exec.Cmd { + c.Assert(name, Equals, "../bin/php") + + cmd := exec.Command(os.Args[0], "-test.run=TestHelperProcess", "--", "exit-code", "0") + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} + // Set the working directory right now so that it can be changed by + // calling test case + cmd.Dir, _ = os.Getwd() + return cmd + } + + home, err := filepath.Abs("testdata/executor") + c.Assert(err, IsNil) + + homedir.Reset() + os.Setenv("HOME", home) + defer homedir.Reset() + + oldwd, _ := os.Getwd() + defer os.Chdir(oldwd) + os.Chdir(filepath.Join(home, "project")) + defer cleanupExecutorTempFiles() + + c.Assert((&Executor{BinName: "php", Args: []string{"php"}}).Execute(true), Equals, 0) + +} + +func (s *ExecutorSuite) TestBinaryOtherThanPhp(c *C) { + defer restoreExecCommand() + execCommand = func(name string, arg ...string) *exec.Cmd { + c.Assert(name, Equals, "not-php") + + cmd := exec.Command(os.Args[0], "-test.run=TestHelperProcess", "--", "exit-code", "0") + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} + // Set the working directory right now so that it can be changed by + // calling test case + cmd.Dir, _ = os.Getwd() + return cmd + } + + home, err := filepath.Abs("testdata/executor") + c.Assert(err, IsNil) + + homedir.Reset() + os.Setenv("HOME", home) + defer homedir.Reset() + + oldwd, _ := os.Getwd() + defer os.Chdir(oldwd) + os.Chdir(filepath.Join(home, "project")) + defer cleanupExecutorTempFiles() + + c.Assert((&Executor{BinName: "php", Args: []string{"not-php"}}).Execute(true), Equals, 0) +} + func (s *ExecutorSuite) TestEnvInjection(c *C) { defer restoreExecCommand() fakeExecCommand("dump-env") @@ -165,10 +230,10 @@ func (s *ExecutorSuite) TestEnvInjection(c *C) { // change the project name to get exposed env vars projectFile := filepath.Join(".platform", "local", "project.yaml") - contents, err := ioutil.ReadFile(projectFile) + contents, err := os.ReadFile(projectFile) c.Assert(err, IsNil) - defer ioutil.WriteFile(projectFile, contents, 0644) - ioutil.WriteFile(projectFile, bytes.Replace(contents, []byte("bew7pfa7t2ut2"), []byte("aew7pfa7t2ut2"), 1), 0644) + defer os.WriteFile(projectFile, contents, 0644) + os.WriteFile(projectFile, bytes.Replace(contents, []byte("bew7pfa7t2ut2"), []byte("aew7pfa7t2ut2"), 1), 0644) output.Reset() outCloser = testStdoutCapture(c, &output) diff --git a/local/php/fpm.go b/local/php/fpm.go index ecbce2ff..e783dcb3 100644 --- a/local/php/fpm.go +++ b/local/php/fpm.go @@ -92,7 +92,7 @@ daemonize = no listen = %s listen.allowed_clients = 127.0.0.1 pm = dynamic -pm.max_children = 5 +pm.max_children = 30 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 @@ -107,13 +107,12 @@ php_admin_flag[log_errors] = on ; we want to expose env vars (like in FOO=bar symfony server:start) clear_env = no + +env['PGGSSENCMODE'] = disable +env['LC_ALL'] = C `, logLevel, logLimit, userConfig, listen, workerConfig) } func (p *Server) fpmConfigFile() string { - path := filepath.Join(p.homeDir, fmt.Sprintf("php/%s/fpm-%s.ini", name(p.projectDir), p.Version.Version)) - if _, err := os.Stat(filepath.Dir(path)); os.IsNotExist(err) { - _ = os.MkdirAll(filepath.Dir(path), 0755) - } - return path + return filepath.Join(p.tempDir, fmt.Sprintf("fpm-%s.ini", p.Version.Version)) } diff --git a/local/php/php_builtin_server.go b/local/php/php_builtin_server.go index 88638d75..3a394e55 100644 --- a/local/php/php_builtin_server.go +++ b/local/php/php_builtin_server.go @@ -21,7 +21,6 @@ package php import ( "fmt" - "os" "path/filepath" ) @@ -61,9 +60,5 @@ require $script; `) func (p *Server) phpRouterFile() string { - path := filepath.Join(p.homeDir, fmt.Sprintf("php/%s-router.php", name(p.projectDir))) - if _, err := os.Stat(filepath.Dir(path)); os.IsNotExist(err) { - _ = os.MkdirAll(filepath.Dir(path), 0755) - } - return path + return filepath.Join(p.tempDir, fmt.Sprintf("%s-router.php", p.Version.Version)) } diff --git a/local/php/php_server.go b/local/php/php_server.go index 676e88a0..56adacd0 100644 --- a/local/php/php_server.go +++ b/local/php/php_server.go @@ -21,13 +21,9 @@ package php import ( "context" - "crypto/sha1" "fmt" - "io" - "io/ioutil" "net" "net/http" - "net/http/httptest" "net/http/httputil" "net/url" "os" @@ -35,14 +31,12 @@ import ( "runtime" "strconv" "strings" - "time" "github.com/pkg/errors" "github.com/rs/xid" "github.com/rs/zerolog" "github.com/symfony-cli/phpstore" "github.com/symfony-cli/symfony-cli/local" - fcgiclient "github.com/symfony-cli/symfony-cli/local/fcgi_client" "github.com/symfony-cli/symfony-cli/local/html" "github.com/symfony-cli/symfony-cli/local/pid" "github.com/symfony-cli/symfony-cli/local/process" @@ -52,7 +46,9 @@ import ( type Server struct { Version *phpstore.Version logger zerolog.Logger - homeDir string + StoppedChan chan bool + appVersion string + tempDir string projectDir string documentRoot string passthru string @@ -63,7 +59,7 @@ type Server struct { var addslashes = strings.NewReplacer("\\", "\\\\", "'", "\\'") // NewServer creates a new PHP server backend -func NewServer(homeDir, projectDir, documentRoot, passthru string, logger zerolog.Logger) (*Server, error) { +func NewServer(homeDir, projectDir, documentRoot, passthru, appVersion string, logger zerolog.Logger) (*Server, error) { logger.Debug().Str("source", "PHP").Msg("Reloading PHP versions") phpStore := phpstore.New(homeDir, true, nil) version, source, warning, err := phpStore.BestVersionForDir(projectDir) @@ -77,32 +73,61 @@ func NewServer(homeDir, projectDir, documentRoot, passthru string, logger zerolo return &Server{ Version: version, logger: logger.With().Str("source", "PHP").Str("php", version.Version).Str("path", version.ServerPath()).Logger(), - homeDir: homeDir, + appVersion: appVersion, projectDir: projectDir, documentRoot: documentRoot, passthru: passthru, + StoppedChan: make(chan bool, 1), }, nil } // Start starts a PHP server func (p *Server) Start(ctx context.Context, pidFile *pid.PidFile) (*pid.PidFile, func() error, error) { - var pathsToRemove []string + p.tempDir = pidFile.TempDirectory() + if _, err := os.Stat(p.tempDir); os.IsNotExist(err) { + if err = os.MkdirAll(p.tempDir, 0755); err != nil { + return nil, nil, err + } + } + port, err := process.FindAvailablePort() if err != nil { p.logger.Debug().Err(err).Msg("unable to find an available port") return nil, nil, err } p.addr = net.JoinHostPort("", strconv.Itoa(port)) + + target, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", port)) + if err != nil { + return nil, nil, errors.WithStack(err) + } + p.proxy = httputil.NewSingleHostReverseProxy(target) + p.proxy.ModifyResponse = func(resp *http.Response) error { + if err, processed := p.processToolbarInResponse(resp); processed { + return err + } + + if err, processed := p.processXSendFile(resp); processed { + return err + } + + return nil + } + p.proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + w.WriteHeader(http.StatusBadGateway) + w.Write([]byte(html.WrapHTML(err.Error(), html.CreateErrorTerminal("# "+err.Error()), ""))) + } + workingDir := p.documentRoot env := []string{} var binName, workerName string var args []string if p.Version.IsFPMServer() { fpmConfigFile := p.fpmConfigFile() - if err := ioutil.WriteFile(fpmConfigFile, []byte(p.defaultFPMConf()), 0644); err != nil { + if err := os.WriteFile(fpmConfigFile, []byte(p.defaultFPMConf()), 0644); err != nil { return nil, nil, errors.WithStack(err) } - pathsToRemove = append(pathsToRemove, fpmConfigFile) + p.proxy.Transport = &cgiTransport{} binName = "php-fpm" workerName = "PHP-FPM" args = []string{p.Version.ServerPath(), "--nodaemonize", "--fpm-config", fpmConfigFile} @@ -110,6 +135,7 @@ func (p *Server) Start(ctx context.Context, pidFile *pid.PidFile) (*pid.PidFile, args = append(args, "--force-stderr") } } else if p.Version.IsCGIServer() { + p.proxy.Transport = &cgiTransport{} // as php-cgi reads the main php.ini file from the current directory, // we want to execute from another directory to be sure that we // are always loading the default PHP configuration @@ -124,24 +150,13 @@ func (p *Server) Start(ctx context.Context, pidFile *pid.PidFile) (*pid.PidFile, args = []string{p.Version.ServerPath(), "-b", strconv.Itoa(port), "-d", "error_log=" + errorLog} } else { routerPath := p.phpRouterFile() - if err := ioutil.WriteFile(routerPath, phprouter, 0644); err != nil { + if err := os.WriteFile(routerPath, phprouter, 0644); err != nil { return nil, nil, errors.WithStack(err) } - pathsToRemove = append(pathsToRemove, routerPath) - addr := "127.0.0.1:" + strconv.Itoa(port) binName = "php" workerName = "PHP" - args = []string{p.Version.ServerPath(), "-S", addr, "-d", "variables_order=EGPCS", routerPath} - target, err := url.Parse(fmt.Sprintf("http://%s", addr)) - if err != nil { - return nil, nil, errors.WithStack(err) - } + args = []string{p.Version.ServerPath(), "-S", "127.0.0.1:" + strconv.Itoa(port), "-d", "variables_order=EGPCS", routerPath} env = append(env, "APP_FRONT_CONTROLLER="+strings.TrimLeft(p.passthru, "/")) - p.proxy = httputil.NewSingleHostReverseProxy(target) - p.proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { - w.WriteHeader(http.StatusBadGateway) - w.Write([]byte(html.WrapHTML(err.Error(), html.CreateErrorTerminal("# "+err.Error()), ""))) - } } e := &Executor{ @@ -149,6 +164,7 @@ func (p *Server) Start(ctx context.Context, pidFile *pid.PidFile) (*pid.PidFile, BinName: binName, Args: args, scriptDir: p.projectDir, + Logger: p.logger, } p.logger.Info().Int("port", port).Msg("listening") @@ -180,9 +196,8 @@ func (p *Server) Start(ctx context.Context, pidFile *pid.PidFile) (*pid.PidFile, return phpPidFile, func() error { defer func() { - for _, path := range pathsToRemove { - os.RemoveAll(path) - } + e.CleanupTemporaryDirectories() + p.StoppedChan <- true }() return errors.Wrap(errors.WithStack(runner.Run()), "PHP server exited unexpectedly") @@ -197,6 +212,12 @@ func (p *Server) Serve(w http.ResponseWriter, r *http.Request, env map[string]st for k, v := range p.generateEnv(r) { env[k] = v } + + // inject our ResponseWriter and our environment into the request's context + // to allow for processing at a later stage + r = r.WithContext(context.WithValue(r.Context(), responseWriterContextKey, w)) + r = r.WithContext(context.WithValue(r.Context(), environmentContextKey, env)) + if p.Version.IsCLIServer() { rid := xid.New().String() r.Header.Add("__SYMFONY_LOCAL_REQUEST_ID__", rid) @@ -205,88 +226,13 @@ func (p *Server) Serve(w http.ResponseWriter, r *http.Request, env map[string]st for k, v := range env { envContent += fmt.Sprintf("$_ENV['%s'] = '%s';\n", addslashes.Replace(k), addslashes.Replace(v)) } - err := errors.WithStack(ioutil.WriteFile(envPath, []byte(envContent), 0644)) + err := errors.WithStack(os.WriteFile(envPath, []byte(envContent), 0644)) if err != nil { return err } defer os.Remove(envPath) - pw := httptest.NewRecorder() - p.proxy.ServeHTTP(pw, r) - return p.writeResponse(w, r, env, pw.Result()) } - return p.serveFastCGI(env, w, r) -} - -func (p *Server) serveFastCGI(env map[string]string, w http.ResponseWriter, r *http.Request) error { - // as the process might have been just created, it might not be ready yet - var fcgi *fcgiclient.FCGIClient - var err error - max := 10 - i := 0 - for { - if fcgi, err = fcgiclient.Dial("tcp", p.addr); err == nil { - break - } - i++ - if i > max { - return errors.Wrapf(err, "unable to connect to the PHP FastCGI process") - } - time.Sleep(time.Millisecond * 50) - } - defer fcgi.Close() - defer r.Body.Close() - - // fetching the response from the fastcgi backend, and check for errors - resp, err := fcgi.Request(env, r.Body) - if err != nil { - return errors.Wrapf(err, "unable to fetch the response from the backend") - } - - // X-SendFile - sendFilename := resp.Header.Get("X-SendFile") - _, err = os.Stat(sendFilename) - if sendFilename != "" && err == nil { - http.ServeFile(w, r, sendFilename) - return nil - } - return p.writeResponse(w, r, env, resp) -} -func (p *Server) writeResponse(w http.ResponseWriter, r *http.Request, env map[string]string, resp *http.Response) error { - defer resp.Body.Close() - if env["SYMFONY_TUNNEL"] != "" && env["SYMFONY_TUNNEL_ENV"] == "" { - p.logger.Warn().Msg("Tunnel to Platform.sh open but environment variables not exposed") - } - bodyModified := false - if r.Method == http.MethodGet && r.Header.Get("x-requested-with") == "XMLHttpRequest" { - var err error - if resp.Body, err = p.tweakToolbar(resp.Body, env); err != nil { - return err - } - bodyModified = true - } - for k, v := range resp.Header { - if bodyModified && strings.ToLower(k) == "content-length" { - // we drop the incoming Content-Length, it will be recomputed by Go automatically anyway - continue - } - for i := 0; i < len(v); i++ { - if w.Header().Get(k) == "" { - w.Header().Set(k, v[i]) - } else { - w.Header().Add(k, v[i]) - } - } - } - w.WriteHeader(resp.StatusCode) - if r.Method != http.MethodHead { - io.Copy(w, resp.Body) - } + p.proxy.ServeHTTP(w, r) return nil } - -func name(dir string) string { - h := sha1.New() - io.WriteString(h, dir) - return fmt.Sprintf("%x", h.Sum(nil)) -} diff --git a/local/php/pie.go b/local/php/pie.go new file mode 100644 index 00000000..9dea548b --- /dev/null +++ b/local/php/pie.go @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2025-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package php + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/symfony-cli/symfony-cli/util" +) + +type PieResult struct { + code int + error error +} + +func (p PieResult) Error() string { + if p.error != nil { + return p.error.Error() + } + + return "" +} + +func (p PieResult) ExitCode() int { + return p.code +} + +func Pie(dir string, args, env []string, stdout, stderr, logger io.Writer, debugLogger zerolog.Logger) PieResult { + e := &Executor{ + Dir: dir, + BinName: "php", + Stdout: stdout, + Stderr: stderr, + SkipNbArgs: -1, + ExtraEnv: env, + Logger: debugLogger, + } + + if piePath := os.Getenv("SYMFONY_PIE_PATH"); piePath != "" { + debugLogger.Debug().Str("SYMFONY_PIE_PATH", piePath).Msg("SYMFONY_PIE_PATH has been defined. User is taking control over PIE detection and execution.") + e.Args = append([]string{piePath}, args...) + } else if path, err := e.findPie(); err == nil && isPHPScript(path) { + e.Args = append([]string{"php", path}, args...) + } else { + reason := "No PIE installation found." + if path != "" { + reason = fmt.Sprintf("Detected PIE file (%s) is not a valid PHAR or PHP script.", path) + } + fmt.Fprintln(logger, " WARNING:", reason) + fmt.Fprintln(logger, " Downloading PIE for you, but it is recommended to install PIE yourself, instructions available at https://github.com/php/pie") + // we don't store it under bin/ to avoid it being found by findPie as we want to only use it as a fallback + binDir := filepath.Join(util.GetHomeDir(), "pie") + if path, err = downloadPie(binDir); err != nil { + return PieResult{ + code: 1, + error: errors.Wrap(err, "unable to find pie, get it at https://github.com/php/pie"), + } + } + e.Args = append([]string{"php", path}, args...) + fmt.Fprintf(logger, " (running %s)\n\n", e.CommandLine()) + } + + ret := e.Execute(false) + if ret != 0 { + return PieResult{ + code: ret, + error: errors.Errorf("unable to run %s", e.CommandLine()), + } + } + return PieResult{} +} + +func findPie(logger zerolog.Logger) (string, error) { + for _, file := range []string{"pie", "pie.phar"} { + logger.Debug().Str("source", "PIE").Msgf(`Looking for PIE in the PATH as "%s"`, file) + if pharPath, _ := LookPath(file); pharPath != "" { + logger.Debug().Str("source", "PIE").Msgf(`Found potential PIE as "%s"`, pharPath) + return pharPath, nil + } + } + + return "", os.ErrNotExist +} + +func downloadPie(dir string) (string, error) { + if err := os.MkdirAll(dir, 0755); err != nil { + return "", err + } + path := filepath.Join(dir, "pie.phar") + if _, err := os.Stat(path); err == nil { + return path, nil + } + + piePhar, err := downloadPiePhar() + if err != nil { + return "", err + } + + err = os.WriteFile(path, piePhar, 0755) + if err != nil { + return "", err + } + + return path, nil +} + +func downloadPiePhar() ([]byte, error) { + resp, err := http.Get("https://github.com/php/pie/releases/latest/download/pie.phar") + if err != nil { + return nil, err + } + defer resp.Body.Close() + return io.ReadAll(resp.Body) +} diff --git a/local/php/platformsh.go b/local/php/platformsh.go deleted file mode 100644 index c256313c..00000000 --- a/local/php/platformsh.go +++ /dev/null @@ -1,65 +0,0 @@ -package php - -import ( - "bytes" - "io/ioutil" - "net/http" - "os" - "path/filepath" - - "github.com/pkg/errors" - "github.com/symfony-cli/terminal" -) - -// Bump whenever we want to be sure we get a recent version of the psh CLI -var internalVersion = []byte("3") - -func InstallPlatformPhar(home string) error { - cacheDir := filepath.Join(os.TempDir(), ".symfony", "platformsh", "cache") - if _, err := os.Stat(cacheDir); err != nil { - if err := os.MkdirAll(cacheDir, 0755); err != nil { - return err - } - } - var versionPath = filepath.Join(cacheDir, "internal_version") - dir := filepath.Join(home, ".platformsh", "bin") - if _, err := os.Stat(filepath.Join(dir, "platform")); err == nil { - // check "API version" (we never upgrade automatically the psh CLI except if we need to if our code would not be compatible with old versions) - if v, err := ioutil.ReadFile(versionPath); err == nil && bytes.Equal(v, internalVersion) { - return nil - } - } - - spinner := terminal.NewSpinner(terminal.Stderr) - spinner.PrefixText = "Download additional CLI tools..." - spinner.Start() - defer spinner.Stop() - resp, err := http.Get("https://platform.sh/cli/installer") - if err != nil { - return err - } - defer resp.Body.Close() - installer, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - - installerPath := filepath.Join(home, "platformsh-installer.php") - ioutil.WriteFile(installerPath, installer, 0666) - defer os.Remove(installerPath) - - var stdout bytes.Buffer - e := &Executor{ - Dir: home, - BinName: "php", - Args: []string{"php", installerPath}, - SkipNbArgs: 1, - Stdout: &stdout, - Stderr: &stdout, - } - if ret := e.Execute(false); ret == 1 { - return errors.Errorf("unable to setup platformsh CLI: %s", stdout.String()) - } - - return ioutil.WriteFile(versionPath, internalVersion, 0644) -} diff --git a/local/php/symfony.go b/local/php/symfony.go new file mode 100644 index 00000000..235a2ccc --- /dev/null +++ b/local/php/symfony.go @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package php + +import ( + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/symfony-cli/symfony-cli/envs" +) + +// SymfonyConsoleExecutor returns an Executor prepared to run Symfony Console. +// It returns an error if no console binary is found. +func SymfonyConsoleExecutor(logger zerolog.Logger, args []string) (*Executor, error) { + dir, err := os.Getwd() + if err != nil { + return nil, errors.WithStack(err) + } + + for { + consolePaths := []string{"bin/console", "app/console"} + if consolePath, isConsolePathSpecified := envs.LookupEnv(dir, "SYMFONY_CONSOLE_PATH"); isConsolePathSpecified { + consolePaths = []string{consolePath} + } + + for _, consolePath := range consolePaths { + logger.Debug().Str("consolePath", consolePath).Str("directory", dir).Msgf("Looking for Symfony console") + consolePath = filepath.Join(dir, consolePath) + if _, err := os.Stat(consolePath); err == nil { + return &Executor{ + BinName: "php", + Logger: logger, + Args: append([]string{"php", consolePath}, args...), + }, nil + } + } + + upDir := filepath.Dir(dir) + if upDir == dir || upDir == "." { + break + } + dir = upDir + } + + return nil, errors.New("No console binary found") +} diff --git a/local/php/testdata/php_scripts/custom-one b/local/php/testdata/php_scripts/custom-one new file mode 100755 index 00000000..e86bf5f1 --- /dev/null +++ b/local/php/testdata/php_scripts/custom-one @@ -0,0 +1,4 @@ +#!/opt/homebrew/Cellar/php/8.2.1_1/bin/php +") @@ -56,7 +92,13 @@ func (p *Server) tweakToolbar(body io.ReadCloser, env map[string]string) (io.Rea return nil, errors.WithStack(err) } if n != len(toolbarHint) || !bytes.Equal(start, toolbarHint) { - return ioutil.NopCloser(io.MultiReader(bytes.NewReader(bn), bytes.NewReader(start), body)), nil + return struct { + io.Reader + io.Closer + }{ + io.MultiReader(bytes.NewReader(bn), bytes.NewReader(start), body), + body, + }, nil } logoBg := "sf-toolbar-status-normal" @@ -66,7 +108,7 @@ func (p *Server) tweakToolbar(body io.ReadCloser, env map[string]string) (io.Rea if env["SYMFONY_TUNNEL"] != "" { tunnel = fmt.Sprintf(`Up (%s)`, env["SYMFONY_TUNNEL"]) if env["SYMFONY_TUNNEL_ENV"] != "" { - envVars = `from Platform.sh` + envVars = fmt.Sprintf(`from %s`, env["SYMFONY_TUNNEL_BRAND"]) logoBg = "sf-toolbar-status-green" } else { logoBg = "sf-toolbar-status-yellow" @@ -81,27 +123,36 @@ func (p *Server) tweakToolbar(body io.ReadCloser, env map[string]string) (io.Rea } } - webmail := `Down` - rabbitmqui := `Down` + webmail := `Webmail Down` + rabbitmqui := `RabbitMQ UI Down` blackfire := `Down` + extraLinks := `` if env, err := envs.NewLocal(p.projectDir, terminal.IsDebug()); err == nil { - values := envs.AsMap(env) - if prefix := env.FindRelationshipPrefix("mailer", "http"); prefix != "" { - if url, exists := values[prefix+"URL"]; exists { - webmail = fmt.Sprintf(`Up   Open`, url) - } + if url, exists := env.FindServiceUrl("mailer"); exists { + webmail = fmt.Sprintf(`Webmail Up`, url) } - if prefix := env.FindRelationshipPrefix("amqp", "http"); prefix != "" { - if url, exists := values[prefix+"URL"]; exists { - rabbitmqui = fmt.Sprintf(`Up   Open`, url) - } + if url, exists := env.FindServiceUrl("amqp"); exists { + rabbitmqui = fmt.Sprintf(`RabbitMQ UI Up`, url) } if prefix := env.FindRelationshipPrefix("blackfire", "tcp"); prefix != "" { - blackfire = `Up   Open` + blackfire = `Up` + } + for _, service := range env.FindHttpServices() { + if service == "mailer-web" || service == "amqp" { + continue + } + + if url, exists := env.FindServiceUrl(service); exists { + extraLinks += fmt.Sprintf(``, url, cases.Title(language.Und).String(service)) + } + } + + if len(extraLinks) > 0 { + extraLinks = `
` + extraLinks } } - b, err := ioutil.ReadAll(body) + b, err := io.ReadAll(body) if err != nil { return body, errors.WithStack(err) } @@ -110,33 +161,34 @@ func (p *Server) tweakToolbar(body io.ReadCloser, env map[string]string) (io.Rea
- - + + Server
-
-
- Server` + p.Version.ServerTypeName() + ` ` + p.Version.Version + ` -
-
- Tunnel` + tunnel + ` -
-
- Docker Compose` + docker + ` -
-
- Env Vars` + envVars + ` -
-
- RabbitMQ UI` + rabbitmqui + ` -
-
- Webmail` + webmail + ` -
-
- Blackfire.io Agent` + blackfire + ` +
+
+
+ Symfony CLI` + p.appVersion + ` +
+
+ PHP` + p.Version.ServerTypeName() + ` ` + p.Version.Version + ` +
+
+ Tunnel` + tunnel + ` +
+
+ Docker Compose` + docker + ` +
+
+ Env Vars` + envVars + ` +
+
` + rabbitmqui + `
+
` + webmail + `
+
+ Blackfire.io Agent` + blackfire + `
+ ` + extraLinks + `
@@ -146,5 +198,11 @@ $1`) re := regexp.MustCompile(`(<(?:a|button)[^"]+?class="hide-button")`) b = re.ReplaceAll(b, content) - return ioutil.NopCloser(io.MultiReader(bytes.NewReader(bn), bytes.NewReader(start), bytes.NewReader(b))), nil + return struct { + io.Reader + io.Closer + }{ + io.MultiReader(bytes.NewReader(bn), bytes.NewReader(start), bytes.NewReader(b)), + body, + }, nil } diff --git a/local/php/utils.go b/local/php/utils.go new file mode 100644 index 00000000..860b73de --- /dev/null +++ b/local/php/utils.go @@ -0,0 +1,30 @@ +package php + +import ( + "bufio" + "bytes" + "os" +) + +// isPHPScript checks that the provided file is indeed a phar/PHP script (not a .bat file) +func isPHPScript(path string) bool { + if path == "" { + return false + } + file, err := os.Open(path) + if err != nil { + return false + } + defer file.Close() + reader := bufio.NewReader(file) + byteSlice, _, err := reader.ReadLine() + if err != nil { + return false + } + + if bytes.Equal(byteSlice, []byte(" + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package php + +import ( + "path/filepath" + + . "gopkg.in/check.v1" +) + +type UtilsSuite struct{} + +var _ = Suite(&UtilsSuite{}) + +func (s *UtilsSuite) TestIsPHPScript(c *C) { + dir, err := filepath.Abs("testdata/php_scripts") + c.Assert(err, IsNil) + + c.Assert(isPHPScript(""), Equals, false) + c.Assert(isPHPScript(filepath.Join(dir, "unknown")), Equals, false) + c.Assert(isPHPScript(filepath.Join(dir, "invalid")), Equals, false) + + for _, validScripts := range []string{ + "usual-one", + "debian-style", + "custom-one", + "plain-one.php", + } { + c.Assert(isPHPScript(filepath.Join(dir, validScripts)), Equals, true) + } +} diff --git a/local/php/xsendfile.go b/local/php/xsendfile.go new file mode 100644 index 00000000..a72673f4 --- /dev/null +++ b/local/php/xsendfile.go @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package php + +import ( + "net/http" + "os" +) + +func (p *Server) processXSendFile(resp *http.Response) (error, bool) { + // X-SendFile + sendFilename := resp.Header.Get("X-SendFile") + if sendFilename == "" { + return nil, false + } else if _, err := os.Stat(sendFilename); err != nil { + return nil, false + } + + req := resp.Request + w := req.Context().Value(responseWriterContextKey).(http.ResponseWriter) + + http.ServeFile(w, req, sendFilename) + + return nil, true +} diff --git a/local/pid/pidfile.go b/local/pid/pidfile.go index 9f211fe3..243a2012 100644 --- a/local/pid/pidfile.go +++ b/local/pid/pidfile.go @@ -24,11 +24,11 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" "syscall" + "time" "github.com/mitchellh/go-homedir" "github.com/pkg/errors" @@ -37,6 +37,8 @@ import ( "github.com/symfony-cli/symfony-cli/util" ) +const WebServerName = "Web Server" + type PidFile struct { Dir string `json:"dir"` Watched []string `json:"watch"` @@ -56,7 +58,7 @@ func New(dir string, args []string) *PidFile { // server or proxy path = filepath.Join(util.GetHomeDir(), "var", name(dir)+".pid") } else { - // workers are stored in a sub-directory + // workers are stored in a subdirectory path = filepath.Join(util.GetHomeDir(), "var", name(dir), name(command)+".pid") } // we need to load the existing file if there is one @@ -72,7 +74,7 @@ func New(dir string, args []string) *PidFile { } func Load(path string) (*PidFile, error) { - contents, err := ioutil.ReadFile(path) + contents, err := os.ReadFile(path) if err != nil { return nil, err } @@ -93,7 +95,7 @@ func (p *PidFile) String() string { return p.CustomName } if p.Args == nil { - return "Web Server" + return WebServerName } return p.Command() } @@ -103,11 +105,52 @@ func (p *PidFile) ShortName() string { return p.CustomName } if len(p.Args) == 0 { - return "Web Server" + return WebServerName } return "Worker " + p.Args[0] } +func (p *PidFile) WaitForExit() error { + if p.Pid == 0 { + return nil + } + + process, err := os.FindProcess(p.Pid) + if err != nil { + return err + } + + defer p.Remove() + ch := make(chan error) + go func() { + if process.Signal(syscall.Signal(0)) != nil { + ch <- nil + return + } + + _, err := process.Wait() + if err == nil { + ch <- nil + return + } + if serr, isSysCallError := err.(*os.SyscallError); isSysCallError { + if errn, isErrno := serr.Err.(syscall.Errno); isErrno && errn == syscall.ECHILD { + ch <- nil + return + } + } + ch <- errors.WithMessagef(err, "while waiting for process %v (%s)", p.Pid, p.ShortName()) + close(ch) + }() + + select { + case err := <-ch: + return err + case _ = <-time.After(30 * time.Second): + return errors.Errorf("Time out detected during \"%s\" process exit", p.ShortName()) + } +} + func (p *PidFile) WaitForPid() <-chan error { ch := make(chan error, 1) @@ -189,6 +232,19 @@ func (p *PidFile) WorkerPidDir() string { return filepath.Join(util.GetHomeDir(), "var", name(p.Dir)) } +func (p *PidFile) TempDirectory() string { + return filepath.Join(util.GetHomeDir(), "php", name(p.Dir)) +} + +func (p *PidFile) CleanupDirectories() { + os.RemoveAll(p.TempDirectory()) + // We don't want to force removal of log and pid files, we only want to + // clean up empty directories. To do so we use `os.Remove` instead of + // `os.RemoveAll` + os.Remove(p.WorkerLogDir()) + os.Remove(p.WorkerPidDir()) +} + func (p *PidFile) LogReader() (io.ReadCloser, error) { logFile := p.LogFile() if err := os.MkdirAll(filepath.Dir(logFile), 0755); err != nil { @@ -255,7 +311,7 @@ func (p *PidFile) Write(pid, port int, scheme string) error { return err } - return ioutil.WriteFile(p.path, b, 0644) + return os.WriteFile(p.path, b, 0644) } // Stop kills the current process @@ -267,7 +323,21 @@ func (p *PidFile) Stop() error { return kill(p.Pid) } -func ToConfiguredProjects() (map[string]*projects.ConfiguredProject, error) { +// Signal sends a signal to the current process for this PidFile +func (p *PidFile) Signal(sig os.Signal) error { + if p.Pid == 0 { + return nil + } + + process, err := os.FindProcess(p.Pid) + if err != nil { + return err + } + + return process.Signal(sig) +} + +func ToConfiguredProjects(shortenHomeDir bool) (map[string]*projects.ConfiguredProject, error) { ps := make(map[string]*projects.ConfiguredProject) userHomeDir, err := homedir.Dir() if err != nil { @@ -279,7 +349,7 @@ func ToConfiguredProjects() (map[string]*projects.ConfiguredProject, error) { } port := pid.Port shortDir := pid.Dir - if strings.HasPrefix(shortDir, userHomeDir) { + if strings.HasPrefix(shortDir, userHomeDir) && shortenHomeDir { shortDir = "~" + shortDir[len(userHomeDir):] } ps[shortDir] = &projects.ConfiguredProject{ @@ -333,7 +403,7 @@ func doAll(dir string) []*PidFile { if !strings.HasSuffix(p, ".pid") { return nil } - contents, err := ioutil.ReadFile(p) + contents, err := os.ReadFile(p) if err != nil { return nil } diff --git a/local/platformsh/applications.go b/local/platformsh/applications.go index 21a61ada..93a9dc0a 100644 --- a/local/platformsh/applications.go +++ b/local/platformsh/applications.go @@ -20,7 +20,6 @@ package platformsh import ( - "io/ioutil" "os" "path/filepath" "sort" @@ -35,9 +34,9 @@ var skippedDirectories = map[string]interface{}{ "vendor": nil, "node_modules": nil, "bundles": nil, - "var": nil, "cache": nil, "config": nil, + "public": nil, "tests": nil, "templates": nil, "assets": nil, @@ -45,6 +44,17 @@ var skippedDirectories = map[string]interface{}{ "fonts": nil, "js": nil, "src": nil, + "var": nil, + "web": nil, +} + +type UpsunDotYaml struct { + Applications map[string]struct { + LocalApplication `yaml:",inline"` + Source struct { + Root string + } + } } // Only a wrapper type around LocalApplication used to get Access to @@ -93,16 +103,35 @@ func FindLocalApplications(rootDirectory string) LocalApplications { return apps } + brand := GuessCloudFromDirectory(rootDirectory) + if brand == NoBrand { + return apps + } go func() { for file := range appParser { - content, err := ioutil.ReadFile(file) + content, err := os.ReadFile(file) if err != nil { terminal.Logger.Warn().Msgf("Could not read %s file: %s\n", file, err) continue } + if brand == UpsunBrand { + var config UpsunDotYaml + if err := yaml.Unmarshal(content, &config); err != nil { + terminal.Logger.Error().Msgf("Could not decode %s YAML file: %s\n", file, err) + continue + } + for name, app := range config.Applications { + app.Name = name + app.DefinitionFile = file + app.LocalRootDir = filepath.Join(rootDirectory, app.Source.Root) + apps = append(apps, app.LocalApplication) + } + continue + } + if strings.HasSuffix(file, filepath.Join(".platform", "applications.yaml")) { - multiApps := ApplicationsDotYaml{} + var multiApps ApplicationsDotYaml if err := yaml.Unmarshal(content, &multiApps); err != nil { terminal.Logger.Error().Msgf("Could not decode %s YAML file: %s\n", file, err) continue @@ -128,7 +157,7 @@ func FindLocalApplications(rootDirectory string) LocalApplications { appParsingDone <- true }() - for _, path := range findAppConfigFiles(rootDirectory) { + for _, path := range findAppConfigFiles(brand, rootDirectory) { appParser <- path } @@ -139,8 +168,22 @@ func FindLocalApplications(rootDirectory string) LocalApplications { return apps } -func findAppConfigFiles(dir string) []string { - dirs := []string{} +func findAppConfigFiles(brand CloudBrand, dir string) []string { + files := []string{} + if brand == UpsunBrand { + dir = filepath.Join(dir, brand.ProjectConfigPath) + fs, err := os.ReadDir(dir) + if err != nil { + return files + } + for _, f := range fs { + if strings.HasSuffix(f.Name(), ".yaml") { + files = append(files, filepath.Join(dir, f.Name())) + } + } + return files + } + separator := string(filepath.Separator) rootDirectoryLen := len(dir) + 1 _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { @@ -160,18 +203,18 @@ func findAppConfigFiles(dir string) []string { } // don't go too deep down the tree - if len(strings.Split(path[rootDirectoryLen:], separator)) > 5 { + if len(strings.Split(path[rootDirectoryLen:], separator)) > 3 { return filepath.SkipDir } } if info.Name() == "applications.yaml" || info.Name() == ".platform.app.yaml" { - dirs = append(dirs, path) + files = append(files, path) } return nil }) - return dirs + return files } func GuessSelectedAppByWd(apps LocalApplications) *LocalApplication { diff --git a/local/platformsh/brand.go b/local/platformsh/brand.go new file mode 100644 index 00000000..0335a491 --- /dev/null +++ b/local/platformsh/brand.go @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package platformsh + +import ( + "os" + "path/filepath" + "strings" +) + +type CloudBrand struct { + Name string + ProjectConfigPath string + CommandPrefix string + CLIConfigPath string + CLIPrefix string + GitRemoteName string + BinName string +} + +var UpsunBrand = CloudBrand{ + Name: "Upsun", + ProjectConfigPath: ".upsun", + CLIConfigPath: ".upsun-cli", + CLIPrefix: "UPSUN_CLI_", + CommandPrefix: "upsun:", + GitRemoteName: "upsun", + BinName: "upsun", +} +var PlatformshBrand = CloudBrand{ + Name: "Platform.sh", + ProjectConfigPath: ".platform", + CLIConfigPath: ".platformsh", + CLIPrefix: "PLATFORMSH_CLI_", + CommandPrefix: "cloud:", + GitRemoteName: "platform", + BinName: "platform", +} + +// NoBrand is used when there is no explicit setting for the brand. +var NoBrand = CloudBrand{ + Name: "", + ProjectConfigPath: "", + CLIConfigPath: ".platformsh", + CLIPrefix: "PLATFORMSH_CLI_", + CommandPrefix: "cloud:", + GitRemoteName: "", + BinName: "platform", +} + +func (b CloudBrand) String() string { + return b.Name +} + +// BinaryPath returns the cloud binary path. +func (b CloudBrand) BinaryPath() string { + return filepath.Join(b.CLIConfigPath, "bin", b.BinName) +} + +func GuessCloudFromCommandName(name string) CloudBrand { + // if the namespace is upsun, then that's the brand we want to use + if strings.HasPrefix(name, "upsun:") { + return UpsunBrand + } + + if dir, err := os.Getwd(); err == nil { + return GuessCloudFromDirectory(dir) + } + + return PlatformshBrand +} + +func GuessCloudFromDirectory(dir string) CloudBrand { + for _, brand := range []CloudBrand{UpsunBrand, PlatformshBrand} { + if _, err := os.Stat(filepath.Join(dir, brand.ProjectConfigPath)); err == nil { + return brand + } + } + return NoBrand +} diff --git a/local/platformsh/cli.go b/local/platformsh/cli.go new file mode 100644 index 00000000..4e5f3e72 --- /dev/null +++ b/local/platformsh/cli.go @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package platformsh + +import ( + "bytes" + _ "embed" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + + "github.com/mitchellh/go-homedir" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/symfony-cli/console" + "github.com/symfony-cli/symfony-cli/util" +) + +var ( + psh *CLI + pshOnce sync.Once +) + +type CLI struct { + Commands []*console.Command + Hooks map[string]console.BeforeFunc + + path string +} + +func Get() (*CLI, error) { + var err error + pshOnce.Do(func() { + psh, err = newCLI() + if err != nil { + err = errors.Wrap(err, "Unable to setup Platform.sh/Upsun CLI") + } + }) + return psh, err +} + +func newCLI() (*CLI, error) { + p := &CLI{ + Hooks: map[string]console.BeforeFunc{}, + } + for _, command := range Commands { + command.Action = p.proxyPSHCmd(strings.TrimPrefix(command.Category+":"+command.Name, "cloud:")) + command.Args = []*console.Arg{ + {Name: "anything", Slice: true, Optional: true}, + } + command.FlagParsing = console.FlagParsingSkipped + command.Flags = append(command.Flags, + &console.BoolFlag{Name: "no", Aliases: []string{"n"}}, + &console.BoolFlag{Name: "yes", Aliases: []string{"y"}}, + ) + p.Commands = append(p.Commands, command) + } + return p, nil +} + +func (p *CLI) AddBeforeHook(name string, f console.BeforeFunc) { + p.Hooks[name] = f + for _, command := range p.Commands { + if command.FullName() == name { + command.FlagParsing = console.FlagParsingNormal + break + } + } +} + +func (p *CLI) getPath(brand CloudBrand) string { + if p.path != "" { + return p.path + } + + home, err := homedir.Dir() + if err != nil { + panic("unable to get home directory") + } + + // the Platform.sh CLI is always available on the containers thanks to the configurator + p.path = filepath.Join(home, brand.BinaryPath()) + if !util.InCloud() { + if cloudPath, err := Install(home, brand); err == nil { + p.path = cloudPath + } + } + return p.path +} + +func (p *CLI) PSHMainCommands() []*console.Command { + names := map[string]bool{ + "cloud:project:list": true, + "cloud:environment:list": true, + "cloud:environment:branch": true, + "cloud:tunnel:open": true, + "cloud:environment:ssh": true, + "cloud:environment:push": true, + "cloud:domain:list": true, + "cloud:variable:list": true, + "cloud:user:add": true, + } + mainCmds := []*console.Command{} + for _, command := range p.Commands { + if names[command.FullName()] { + mainCmds = append(mainCmds, command) + } + } + return mainCmds +} + +func (p *CLI) proxyPSHCmd(commandName string) console.ActionFunc { + return func(commandName string) console.ActionFunc { + return func(ctx *console.Context) error { + if hook, ok := p.Hooks[commandName]; ok && !console.IsHelp(ctx) { + if err := hook(ctx); err != nil { + return err + } + } + brand := GuessCloudFromCommandName(ctx.Command.UserName) + args := os.Args[1:] + for i := range args { + if args[i] == ctx.Command.UserName { + args[i] = commandName + break + } + } + return p.executor(brand, args).Run() + } + }(commandName) +} + +func (p *CLI) executor(brand CloudBrand, args []string) *exec.Cmd { + prefix := brand.CLIPrefix + + env := []string{ + fmt.Sprintf("%sAPPLICATION_NAME=%s CLI for Symfony", prefix, brand), + fmt.Sprintf("%sAPPLICATION_EXECUTABLE=symfony", prefix), + "XDEBUG_MODE=off", + fmt.Sprintf("%sWRAPPED=1", prefix), + } + if util.InCloud() { + env = append(env, fmt.Sprintf("%sUPDATES_CHECK=0", prefix)) + } + args[0] = strings.TrimPrefix(strings.TrimPrefix(args[0], "upsun:"), "cloud:") + cmd := exec.Command(p.getPath(brand), args...) + cmd.Env = append(os.Environ(), env...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return cmd +} + +func (p *CLI) RunInteractive(logger zerolog.Logger, projectDir string, args []string, debug bool, stdin io.Reader) (bytes.Buffer, bool) { + var buf bytes.Buffer + brand := GuessCloudFromCommandName(args[0]) + cmd := p.executor(brand, args) + if projectDir != "" { + cmd.Dir = projectDir + } + if debug { + cmd.Stdout = io.MultiWriter(&buf, os.Stdout) + cmd.Stderr = io.MultiWriter(&buf, os.Stderr) + } else { + cmd.Stdout = &buf + cmd.Stderr = &buf + } + if stdin != nil { + cmd.Stdin = stdin + } + logger.Debug().Str("cmd", strings.Join(cmd.Args, " ")).Msgf("Executing %s CLI command interactively", GuessCloudFromCommandName(args[0])) + if err := cmd.Run(); err != nil { + return buf, false + } + return buf, true +} + +func (p *CLI) WrapHelpPrinter() func(w io.Writer, templ string, data interface{}) { + currentHelpPrinter := console.HelpPrinter + return func(w io.Writer, templ string, data interface{}) { + switch cmd := data.(type) { + case *console.Command: + if strings.HasPrefix(cmd.Category, "cloud") { + brand := GuessCloudFromCommandName(cmd.UserName) + cmd := p.executor(brand, []string{cmd.FullName(), "--help"}) + cmd.Run() + } else { + currentHelpPrinter(w, templ, data) + } + default: + currentHelpPrinter(w, templ, data) + } + } +} diff --git a/local/platformsh/commands.go b/local/platformsh/commands.go index 5907f9ec..56b8f13f 100644 --- a/local/platformsh/commands.go +++ b/local/platformsh/commands.go @@ -30,2280 +30,3026 @@ var Commands = []*console.Command{ { Category: "cloud", Name: "_completion", - Usage: "BASH completion hook.", - Hidden: console.Hide, - Flags: []console.Flag{ - &console.BoolFlag{Name: "generate-hook", Aliases: []string{"g"},}, - &console.BoolFlag{Name: "multiple", Aliases: []string{"m"},}, - &console.StringFlag{Name: "program", Aliases: []string{"p"},}, - &console.StringFlag{Name: "shell-type",}, + Aliases: []*console.Alias{ + {Name: "upsun:_completion", Hidden: true}, + }, + Usage: "BASH completion hook.", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.BoolFlag{Name: "generate-hook", Aliases: []string{"g"}}, + &console.BoolFlag{Name: "multiple", Aliases: []string{"m"}}, + &console.StringFlag{Name: "program", Aliases: []string{"p"}}, + &console.StringFlag{Name: "shell-type"}, }, }, { Category: "cloud", Name: "bot", - Usage: "The Platform.sh Bot", - Hidden: console.Hide, - Flags: []console.Flag{ - &console.BoolFlag{Name: "parrot",}, - &console.BoolFlag{Name: "party",}, + Aliases: []*console.Alias{ + {Name: "upsun:bot", Hidden: true}, + }, + Usage: "The Platform.sh/Upsun Bot", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.BoolFlag{Name: "parrot"}, + &console.BoolFlag{Name: "party"}, }, }, { Category: "cloud", Name: "clear-cache", - Aliases: []*console.Alias{ - {Name: "cloud:clearcache"}, + Aliases: []*console.Alias{ + {Name: "upsun:clear-cache", Hidden: true}, {Name: "cloud:cc"}, + {Name: "upsun:cc", Hidden: true}, }, - Usage: "Clear the CLI cache", + Usage: "Clear the CLI cache", }, { Category: "cloud", - Name: "docs", - Usage: "Open the online documentation", - Flags: []console.Flag{ - &console.StringFlag{Name: "browser",}, - &console.BoolFlag{Name: "pipe",}, + Name: "console", + Aliases: []*console.Alias{ + {Name: "upsun:console", Hidden: true}, + {Name: "cloud:web"}, + {Name: "upsun:web", Hidden: true}, + }, + Usage: "Open the project in the Console", + Flags: []console.Flag{ + &console.StringFlag{Name: "browser"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "pipe"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud", - Name: "legacy-migrate", - Usage: "Migrate from the legacy file structure", - Hidden: console.Hide, - Flags: []console.Flag{ - &console.BoolFlag{Name: "no-backup",}, + Name: "docs", + Aliases: []*console.Alias{ + {Name: "upsun:docs", Hidden: true}, + }, + Usage: "Open the online documentation", + Flags: []console.Flag{ + &console.StringFlag{Name: "browser"}, + &console.BoolFlag{Name: "pipe"}, }, }, { Category: "cloud", - Name: "multi", - Usage: "Execute a command on multiple projects", - Flags: []console.Flag{ - &console.BoolFlag{Name: "continue",}, - &console.StringFlag{Name: "projects", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "reverse",}, - &console.StringFlag{Name: "sort", DefaultValue: "title",}, + Name: "legacy-migrate", + Aliases: []*console.Alias{ + {Name: "upsun:legacy-migrate", Hidden: true}, + }, + Usage: "Migrate from the legacy file structure", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.BoolFlag{Name: "no-backup"}, }, }, { Category: "cloud", - Name: "web", - Usage: "Open the Web UI", - Flags: []console.Flag{ - &console.StringFlag{Name: "browser",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "pipe",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Name: "multi", + Aliases: []*console.Alias{ + {Name: "upsun:multi", Hidden: true}, + }, + Usage: "Execute a command on multiple projects", + Flags: []console.Flag{ + &console.BoolFlag{Name: "continue"}, + &console.StringFlag{Name: "projects", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "reverse"}, + &console.StringFlag{Name: "sort", DefaultValue: "title"}, }, }, { Category: "cloud", Name: "welcome", - Usage: "Welcome to Platform.sh", - Hidden: console.Hide, + Aliases: []*console.Alias{ + {Name: "upsun:welcome", Hidden: true}, + }, + Usage: "Welcome to Platform.sh/Upsun", + Hidden: console.Hide, }, { Category: "cloud", Name: "winky", - Usage: "", - Hidden: console.Hide, + Aliases: []*console.Alias{ + {Name: "upsun:winky", Hidden: true}, + }, + Usage: "", + Hidden: console.Hide, }, { Category: "cloud:activity", Name: "cancel", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "activity:cancel", Hidden: true}, + {Name: "upsun:activity:cancel", Hidden: true}, }, - Usage: "Cancel an activity", - Flags: []console.Flag{ - &console.BoolFlag{Name: "all", Aliases: []string{"a"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "exclude-type",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "type",}, + Usage: "Cancel an activity", + Flags: []console.Flag{ + &console.BoolFlag{Name: "all", Aliases: []string{"a"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "exclude-type", Aliases: []string{"x"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "type", Aliases: []string{"t"}}, }, }, { Category: "cloud:activity", Name: "get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "activity:get", Hidden: true}, - }, - Usage: "View detailed information on a single activity", - Flags: []console.Flag{ - &console.BoolFlag{Name: "all", Aliases: []string{"a"},}, - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "exclude-type",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "incomplete", Aliases: []string{"i"},}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, - &console.StringFlag{Name: "result",}, - &console.StringFlag{Name: "state",}, - &console.StringFlag{Name: "type",}, + {Name: "upsun:activity:get", Hidden: true}, + }, + Usage: "View detailed information on a single activity", + Flags: []console.Flag{ + &console.BoolFlag{Name: "all", Aliases: []string{"a"}}, + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "exclude-type", Aliases: []string{"x"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "incomplete", Aliases: []string{"i"}}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, + &console.StringFlag{Name: "result"}, + &console.StringFlag{Name: "state"}, + &console.StringFlag{Name: "type", Aliases: []string{"t"}}, }, }, { Category: "cloud:activity", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "activity:list", Hidden: true}, + {Name: "upsun:activity:list", Hidden: true}, {Name: "cloud:activities"}, + {Name: "upsun:activities", Hidden: true}, {Name: "activities", Hidden: true}, {Name: "cloud:act"}, + {Name: "upsun:act", Hidden: true}, {Name: "act", Hidden: true}, }, - Usage: "Get a list of activities for an environment or project", - Flags: []console.Flag{ - &console.BoolFlag{Name: "all", Aliases: []string{"a"},}, - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "exclude-type", Aliases: []string{"x"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "incomplete", Aliases: []string{"i"},}, - &console.StringFlag{Name: "limit",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "result",}, - &console.StringFlag{Name: "start",}, - &console.StringFlag{Name: "state",}, - &console.StringFlag{Name: "type", Aliases: []string{"t"},}, + Usage: "Get a list of activities for an environment or project", + Flags: []console.Flag{ + &console.BoolFlag{Name: "all", Aliases: []string{"a"}}, + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "exclude-type", Aliases: []string{"x"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "incomplete", Aliases: []string{"i"}}, + &console.StringFlag{Name: "limit"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "result"}, + &console.StringFlag{Name: "start"}, + &console.StringFlag{Name: "state"}, + &console.StringFlag{Name: "type", Aliases: []string{"t"}}, }, }, { Category: "cloud:activity", Name: "log", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "activity:log", Hidden: true}, + {Name: "upsun:activity:log", Hidden: true}, }, - Usage: "Display the log for an activity", - Flags: []console.Flag{ - &console.BoolFlag{Name: "all", Aliases: []string{"a"},}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "exclude-type",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "incomplete", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "refresh",}, - &console.StringFlag{Name: "result",}, - &console.StringFlag{Name: "state",}, - &console.BoolFlag{Name: "timestamps", Aliases: []string{"t"},}, - &console.StringFlag{Name: "type",}, + Usage: "Display the log for an activity", + Flags: []console.Flag{ + &console.BoolFlag{Name: "all", Aliases: []string{"a"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "exclude-type", Aliases: []string{"x"}}, + &console.BoolFlag{Name: "incomplete", Aliases: []string{"i"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "refresh"}, + &console.StringFlag{Name: "result"}, + &console.StringFlag{Name: "state"}, + &console.BoolFlag{Name: "timestamps", Aliases: []string{"t"}}, + &console.StringFlag{Name: "type"}, }, }, { Category: "cloud:api", Name: "curl", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "api:curl", Hidden: true}, + {Name: "upsun:api:curl", Hidden: true}, }, - Usage: "Run an authenticated cURL request on the Platform.sh API", - Hidden: console.Hide, - Flags: []console.Flag{ - &console.StringFlag{Name: "data", Aliases: []string{"d"},}, - &console.BoolFlag{Name: "disable-compression",}, - &console.BoolFlag{Name: "enable-glob",}, - &console.BoolFlag{Name: "fail", Aliases: []string{"f"},}, - &console.BoolFlag{Name: "head", Aliases: []string{"I"},}, - &console.StringFlag{Name: "header", Aliases: []string{"H"},}, - &console.BoolFlag{Name: "include", Aliases: []string{"i"},}, - &console.StringFlag{Name: "request", Aliases: []string{"X"},}, + Usage: "Run an authenticated cURL request on the Platform.sh/Upsun API", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "data", Aliases: []string{"d"}}, + &console.BoolFlag{Name: "disable-compression"}, + &console.BoolFlag{Name: "enable-glob"}, + &console.BoolFlag{Name: "fail", Aliases: []string{"f"}}, + &console.BoolFlag{Name: "head", Aliases: []string{"I"}}, + &console.StringFlag{Name: "header", Aliases: []string{"H"}}, + &console.BoolFlag{Name: "include", Aliases: []string{"i"}}, + &console.StringFlag{Name: "json"}, + &console.BoolFlag{Name: "no-retry-401"}, + &console.StringFlag{Name: "request", Aliases: []string{"X"}}, }, }, { Category: "cloud:app", Name: "config-get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "app:config-get", Hidden: true}, + {Name: "upsun:app:config-get", Hidden: true}, }, - Usage: "View the configuration of an app", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, - &console.BoolFlag{Name: "refresh",}, + Usage: "View the configuration of an app", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "identity-file", Aliases: []string{"i"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, + &console.BoolFlag{Name: "refresh"}, + }, + }, + { + Category: "cloud:app", + Name: "config-validate", + Aliases: []*console.Alias{ + {Name: "app:config-validate", Hidden: true}, + {Name: "upsun:app:config-validate", Hidden: true}, }, + Usage: "Validate the config files of a project", }, { Category: "cloud:app", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "app:list", Hidden: true}, + {Name: "upsun:app:list", Hidden: true}, {Name: "cloud:apps"}, + {Name: "upsun:apps", Hidden: true}, {Name: "apps", Hidden: true}, }, - Usage: "List apps in the project", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "refresh",}, + Usage: "List apps in the project", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.BoolFlag{Name: "pipe"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "refresh"}, }, }, { Category: "cloud:auth", Name: "api-token-login", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "auth:api-token-login", Hidden: true}, + {Name: "upsun:auth:api-token-login", Hidden: true}, }, - Usage: "Log in to Platform.sh using an API token", + Usage: "Log in to Platform.sh/Upsun using an API token", }, { Category: "cloud:auth", Name: "browser-login", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "auth:browser-login", Hidden: true}, + {Name: "upsun:auth:browser-login", Hidden: true}, {Name: "cloud:login"}, + {Name: "upsun:login", Hidden: true}, {Name: "login", Hidden: true}, }, - Usage: "Log in to Platform.sh via a browser", - Flags: []console.Flag{ - &console.StringFlag{Name: "browser",}, - &console.BoolFlag{Name: "force", Aliases: []string{"f"},}, - &console.BoolFlag{Name: "pipe",}, + Usage: "Log in to Platform.sh/Upsun via a browser", + Flags: []console.Flag{ + &console.StringFlag{Name: "browser"}, + &console.BoolFlag{Name: "force", Aliases: []string{"f"}}, + &console.StringFlag{Name: "max-age"}, + &console.StringFlag{Name: "method"}, + &console.BoolFlag{Name: "pipe"}, }, }, { Category: "cloud:auth", Name: "info", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "auth:info", Hidden: true}, + {Name: "upsun:auth:info", Hidden: true}, }, - Usage: "Display your account information", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.BoolFlag{Name: "no-auto-login",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, - &console.BoolFlag{Name: "refresh",}, + Usage: "Display your account information", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-auto-login"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, + &console.BoolFlag{Name: "refresh"}, }, }, { Category: "cloud:auth", Name: "logout", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "auth:logout", Hidden: true}, + {Name: "upsun:auth:logout", Hidden: true}, {Name: "cloud:logout"}, + {Name: "upsun:logout", Hidden: true}, {Name: "logout", Hidden: true}, }, - Usage: "Log out of Platform.sh", - Flags: []console.Flag{ - &console.BoolFlag{Name: "all", Aliases: []string{"a"},}, - &console.BoolFlag{Name: "other",}, + Usage: "Log out of Platform.sh/Upsun", + Flags: []console.Flag{ + &console.BoolFlag{Name: "all", Aliases: []string{"a"}}, + &console.BoolFlag{Name: "other"}, }, }, { Category: "cloud:auth", Name: "token", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "auth:token", Hidden: true}, + {Name: "upsun:auth:token", Hidden: true}, }, - Usage: "Obtain an OAuth 2 access token for requests to Platform.sh APIs", - Hidden: console.Hide, + Usage: "Obtain an OAuth 2 access token for requests to Platform.sh/Upsun APIs", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.BoolFlag{Name: "header", Aliases: []string{"H"}}, + &console.BoolFlag{Name: "no-warn", Aliases: []string{"W"}}, + }, + }, + { + Category: "cloud:auth", + Name: "verify-phone-number", + Aliases: []*console.Alias{ + {Name: "auth:verify-phone-number", Hidden: true}, + {Name: "upsun:auth:verify-phone-number", Hidden: true}, + }, + Usage: "Verify your phone number interactively", }, { Category: "cloud:backup", Name: "create", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "backup:create", Hidden: true}, + {Name: "upsun:backup:create", Hidden: true}, {Name: "cloud:backup"}, + {Name: "upsun:backup", Hidden: true}, {Name: "backup", Hidden: true}, - {Name: "cloud:snapshot:create"}, - {Name: "snapshot:create", Hidden: true}, - {Name: "cloud:environment:backup"}, - {Name: "environment:backup", Hidden: true}, }, - Usage: "Make a backup of an environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "live",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "unsafe",}, - &console.BoolFlag{Name: "wait",}, + Usage: "Make a backup of an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "live"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, + }, + }, + { + Category: "cloud:backup", + Name: "delete", + Aliases: []*console.Alias{ + {Name: "backup:delete", Hidden: true}, + {Name: "upsun:backup:delete", Hidden: true}, + }, + Usage: "Delete an environment backup", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, + }, + }, + { + Category: "cloud:backup", + Name: "get", + Aliases: []*console.Alias{ + {Name: "backup:get", Hidden: true}, + {Name: "upsun:backup:get", Hidden: true}, + }, + Usage: "View an environment backup", + Flags: []console.Flag{ + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, }, }, { Category: "cloud:backup", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "backup:list", Hidden: true}, + {Name: "upsun:backup:list", Hidden: true}, {Name: "cloud:backups"}, + {Name: "upsun:backups", Hidden: true}, {Name: "backups", Hidden: true}, - {Name: "cloud:snapshots"}, - {Name: "snapshots", Hidden: true}, - {Name: "cloud:snapshot:list"}, - {Name: "snapshot:list", Hidden: true}, }, - Usage: "List available backups of an environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "limit",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "start",}, + Usage: "List available backups of an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:backup", Name: "restore", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "backup:restore", Hidden: true}, - {Name: "cloud:environment:restore"}, - {Name: "environment:restore", Hidden: true}, - {Name: "cloud:snapshot:restore"}, - {Name: "snapshot:restore", Hidden: true}, + {Name: "upsun:backup:restore", Hidden: true}, + }, + Usage: "Restore an environment backup", + Flags: []console.Flag{ + &console.StringFlag{Name: "branch-from"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-code"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "target"}, + &console.BoolFlag{Name: "wait"}, + }, + }, + { + Category: "cloud:blue-green", + Name: "conclude", + Aliases: []*console.Alias{ + {Name: "blue-green:conclude", Hidden: true}, + {Name: "upsun:blue-green:conclude", Hidden: true}, + }, + Usage: "ALPHA Conclude a blue/green deployment", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + }, + }, + { + Category: "cloud:blue-green", + Name: "deploy", + Aliases: []*console.Alias{ + {Name: "blue-green:deploy", Hidden: true}, + {Name: "upsun:blue-green:deploy", Hidden: true}, + }, + Usage: "ALPHA Perform a blue/green deployment", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "routing-percentage"}, + }, + }, + { + Category: "cloud:blue-green", + Name: "enable", + Aliases: []*console.Alias{ + {Name: "blue-green:enable", Hidden: true}, + {Name: "upsun:blue-green:enable", Hidden: true}, }, - Usage: "Restore an environment backup", - Flags: []console.Flag{ - &console.StringFlag{Name: "branch-from",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "target",}, - &console.BoolFlag{Name: "wait",}, + Usage: "ALPHA Enable blue/green deployments", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "routing-percentage", Aliases: []string{"%"}}, }, }, { Category: "cloud:certificate", Name: "add", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "certificate:add", Hidden: true}, + {Name: "upsun:certificate:add", Hidden: true}, }, - Usage: "Add an SSL certificate to the project", - Flags: []console.Flag{ - &console.StringFlag{Name: "cert",}, - &console.StringFlag{Name: "chain",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "key",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Add an SSL certificate to the project", + Flags: []console.Flag{ + &console.StringFlag{Name: "cert"}, + &console.StringFlag{Name: "chain"}, + &console.StringFlag{Name: "key"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:certificate", Name: "delete", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "certificate:delete", Hidden: true}, + {Name: "upsun:certificate:delete", Hidden: true}, }, - Usage: "Delete a certificate from the project", - Flags: []console.Flag{ - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Delete a certificate from the project", + Flags: []console.Flag{ + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:certificate", Name: "get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "certificate:get", Hidden: true}, + {Name: "upsun:certificate:get", Hidden: true}, }, - Usage: "View a certificate", - Flags: []console.Flag{ - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, + Usage: "View a certificate", + Flags: []console.Flag{ + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, }, }, { Category: "cloud:certificate", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "certificate:list", Hidden: true}, + {Name: "upsun:certificate:list", Hidden: true}, {Name: "cloud:certificates"}, + {Name: "upsun:certificates", Hidden: true}, {Name: "certificates", Hidden: true}, {Name: "cloud:certs"}, + {Name: "upsun:certs", Hidden: true}, {Name: "certs", Hidden: true}, }, - Usage: "List project certificates", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "domain",}, - &console.StringFlag{Name: "exclude-domain",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "ignore-expiry",}, - &console.StringFlag{Name: "issuer",}, - &console.BoolFlag{Name: "no-auto",}, - &console.BoolFlag{Name: "no-expired",}, - &console.BoolFlag{Name: "no-header",}, - &console.BoolFlag{Name: "only-auto",}, - &console.BoolFlag{Name: "only-expired",}, - &console.BoolFlag{Name: "pipe-domains",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "List project certificates", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "domain"}, + &console.StringFlag{Name: "exclude-domain"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "ignore-expiry"}, + &console.StringFlag{Name: "issuer"}, + &console.BoolFlag{Name: "no-auto"}, + &console.BoolFlag{Name: "no-expired"}, + &console.BoolFlag{Name: "no-header"}, + &console.BoolFlag{Name: "only-auto"}, + &console.BoolFlag{Name: "only-expired"}, + &console.BoolFlag{Name: "pipe-domains"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:commit", Name: "get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "commit:get", Hidden: true}, + {Name: "upsun:commit:get", Hidden: true}, }, - Usage: "Show commit details", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, + Usage: "Show commit details", + Flags: []console.Flag{ + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, }, }, { Category: "cloud:commit", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "commit:list", Hidden: true}, + {Name: "upsun:commit:list", Hidden: true}, {Name: "cloud:commits"}, + {Name: "upsun:commits", Hidden: true}, {Name: "commits", Hidden: true}, }, - Usage: "List commits", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "limit",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "List commits", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.StringFlag{Name: "limit"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:db", Name: "dump", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "db:dump", Hidden: true}, - {Name: "cloud:sql-dump"}, - {Name: "sql-dump", Hidden: true}, - {Name: "cloud:environment:sql-dump"}, - {Name: "environment:sql-dump", Hidden: true}, - }, - Usage: "Create a local dump of the remote database", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "charset",}, - &console.StringFlag{Name: "directory", Aliases: []string{"d"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "exclude-table",}, - &console.StringFlag{Name: "file", Aliases: []string{"f"},}, - &console.BoolFlag{Name: "gzip", Aliases: []string{"z"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "relationship", Aliases: []string{"r"},}, - &console.StringFlag{Name: "schema",}, - &console.BoolFlag{Name: "schema-only",}, - &console.BoolFlag{Name: "stdout", Aliases: []string{"o"},}, - &console.StringFlag{Name: "table",}, - &console.BoolFlag{Name: "timestamp", Aliases: []string{"t"},}, - }, - }, - { - Category: "cloud:db", - Name: "size", - Aliases: []*console.Alias{ - {Name: "db:size", Hidden: true}, - }, - Usage: "Estimate the disk usage of a database", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.BoolFlag{Name: "bytes", Aliases: []string{"B"},}, - &console.BoolFlag{Name: "cleanup", Aliases: []string{"C"},}, - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "relationship", Aliases: []string{"r"},}, + {Name: "upsun:db:dump", Hidden: true}, + }, + Usage: "Create a local dump of the remote database", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "charset"}, + &console.StringFlag{Name: "directory", Aliases: []string{"d"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "exclude-table"}, + &console.StringFlag{Name: "file", Aliases: []string{"f"}}, + &console.BoolFlag{Name: "gzip", Aliases: []string{"z"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "relationship", Aliases: []string{"r"}}, + &console.StringFlag{Name: "schema"}, + &console.BoolFlag{Name: "schema-only"}, + &console.BoolFlag{Name: "stdout", Aliases: []string{"o"}}, + &console.StringFlag{Name: "table"}, + &console.BoolFlag{Name: "timestamp", Aliases: []string{"t"}}, }, }, { Category: "cloud:db", Name: "sql", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "db:sql", Hidden: true}, + {Name: "upsun:db:sql", Hidden: true}, {Name: "cloud:sql"}, + {Name: "upsun:sql", Hidden: true}, {Name: "sql", Hidden: true}, - {Name: "cloud:environment:sql"}, - {Name: "environment:sql", Hidden: true}, }, - Usage: "Run SQL on the remote database", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "raw",}, - &console.StringFlag{Name: "relationship", Aliases: []string{"r"},}, - &console.StringFlag{Name: "schema",}, + Usage: "Run SQL on the remote database", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "raw"}, + &console.StringFlag{Name: "relationship", Aliases: []string{"r"}}, + &console.StringFlag{Name: "schema"}, }, }, { Category: "cloud:domain", Name: "add", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "domain:add", Hidden: true}, + {Name: "upsun:domain:add", Hidden: true}, }, - Usage: "Add a new domain to the project", - Flags: []console.Flag{ - &console.StringFlag{Name: "cert",}, - &console.StringFlag{Name: "chain",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "key",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Add a new domain to the project", + Flags: []console.Flag{ + &console.StringFlag{Name: "attach"}, + &console.StringFlag{Name: "cert"}, + &console.StringFlag{Name: "chain"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "key"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:domain", Name: "delete", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "domain:delete", Hidden: true}, + {Name: "upsun:domain:delete", Hidden: true}, }, - Usage: "Delete a domain from the project", - Flags: []console.Flag{ - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Delete a domain from the project", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:domain", Name: "get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "domain:get", Hidden: true}, + {Name: "upsun:domain:get", Hidden: true}, }, - Usage: "Show detailed information for a domain", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, + Usage: "Show detailed information for a domain", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, }, }, { Category: "cloud:domain", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "domain:list", Hidden: true}, + {Name: "upsun:domain:list", Hidden: true}, {Name: "cloud:domains"}, + {Name: "upsun:domains", Hidden: true}, {Name: "domains", Hidden: true}, }, - Usage: "Get a list of all domains", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "Get a list of all domains", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:domain", Name: "update", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "domain:update", Hidden: true}, + {Name: "upsun:domain:update", Hidden: true}, }, - Usage: "Update a domain", - Flags: []console.Flag{ - &console.StringFlag{Name: "cert",}, - &console.StringFlag{Name: "chain",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "key",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Update a domain", + Flags: []console.Flag{ + &console.StringFlag{Name: "cert"}, + &console.StringFlag{Name: "chain"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "key"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "activate", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:activate", Hidden: true}, + {Name: "upsun:environment:activate", Hidden: true}, }, - Usage: "Activate an environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "parent",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Activate an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "parent"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "branch", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:branch", Hidden: true}, + {Name: "upsun:environment:branch", Hidden: true}, {Name: "cloud:branch"}, + {Name: "upsun:branch", Hidden: true}, {Name: "branch", Hidden: true}, }, - Usage: "Branch an environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.BoolFlag{Name: "force",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.BoolFlag{Name: "no-clone-parent",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "title",}, - &console.StringFlag{Name: "type",}, - &console.BoolFlag{Name: "wait",}, + Usage: "Branch an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-checkout"}, + &console.BoolFlag{Name: "no-clone-parent"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "title"}, + &console.StringFlag{Name: "type"}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "checkout", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:checkout", Hidden: true}, + {Name: "upsun:environment:checkout", Hidden: true}, {Name: "cloud:checkout"}, + {Name: "upsun:checkout", Hidden: true}, {Name: "checkout", Hidden: true}, }, - Usage: "Check out an environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - }, + Usage: "Check out an environment", }, { Category: "cloud:environment", Name: "curl", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:curl", Hidden: true}, - }, - Usage: "Run an authenticated cURL request on an environment's API", - Hidden: console.Hide, - Flags: []console.Flag{ - &console.StringFlag{Name: "data", Aliases: []string{"d"},}, - &console.BoolFlag{Name: "disable-compression",}, - &console.BoolFlag{Name: "enable-glob",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.BoolFlag{Name: "fail", Aliases: []string{"f"},}, - &console.BoolFlag{Name: "head", Aliases: []string{"I"},}, - &console.StringFlag{Name: "header", Aliases: []string{"H"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "include", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "request", Aliases: []string{"X"},}, + {Name: "upsun:environment:curl", Hidden: true}, + }, + Usage: "Run an authenticated cURL request on an environment's API", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "data", Aliases: []string{"d"}}, + &console.BoolFlag{Name: "disable-compression"}, + &console.BoolFlag{Name: "enable-glob"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "fail", Aliases: []string{"f"}}, + &console.BoolFlag{Name: "head", Aliases: []string{"I"}}, + &console.StringFlag{Name: "header", Aliases: []string{"H"}}, + &console.BoolFlag{Name: "include", Aliases: []string{"i"}}, + &console.StringFlag{Name: "json"}, + &console.BoolFlag{Name: "no-retry-401"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "request", Aliases: []string{"X"}}, }, }, { Category: "cloud:environment", Name: "delete", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:delete", Hidden: true}, - {Name: "cloud:environment:deactivate"}, - {Name: "environment:deactivate", Hidden: true}, - }, - Usage: "Delete an environment", - Flags: []console.Flag{ - &console.BoolFlag{Name: "delete-branch",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "exclude",}, - &console.StringFlag{Name: "exclude-type",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "inactive",}, - &console.BoolFlag{Name: "merged",}, - &console.BoolFlag{Name: "no-delete-branch",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "type",}, - &console.BoolFlag{Name: "wait",}, + {Name: "upsun:environment:delete", Hidden: true}, + }, + Usage: "Delete one or more environments", + Flags: []console.Flag{ + &console.BoolFlag{Name: "allow-delete-parent"}, + &console.BoolFlag{Name: "delete-branch"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "exclude"}, + &console.StringFlag{Name: "exclude-status"}, + &console.StringFlag{Name: "exclude-type"}, + &console.BoolFlag{Name: "inactive"}, + &console.BoolFlag{Name: "merged"}, + &console.BoolFlag{Name: "no-delete-branch"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "only-status"}, + &console.StringFlag{Name: "only-type", Aliases: []string{"t"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "status"}, + &console.StringFlag{Name: "type"}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "http-access", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:http-access", Hidden: true}, + {Name: "upsun:environment:http-access", Hidden: true}, {Name: "cloud:httpaccess"}, + {Name: "upsun:httpaccess", Hidden: true}, {Name: "httpaccess", Hidden: true}, }, - Usage: "Update HTTP access settings for an environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "access",}, - &console.StringFlag{Name: "auth",}, - &console.StringFlag{Name: "enabled",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Update HTTP access settings for an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "access"}, + &console.StringFlag{Name: "auth"}, + &console.StringFlag{Name: "enabled"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "info", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:info", Hidden: true}, - {Name: "cloud:environment:metadata"}, - {Name: "environment:metadata", Hidden: true}, + {Name: "upsun:environment:info", Hidden: true}, }, - Usage: "Read or set properties for an environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "refresh",}, - &console.BoolFlag{Name: "wait",}, + Usage: "Read or set properties for an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "refresh"}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "init", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:init", Hidden: true}, + {Name: "upsun:environment:init", Hidden: true}, }, - Usage: "Initialize an environment from a public Git repository", - Flags: []console.Flag{ - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "profile",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Initialize an environment from a public Git repository", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "profile"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:list", Hidden: true}, + {Name: "upsun:environment:list", Hidden: true}, {Name: "cloud:environments"}, + {Name: "upsun:environments", Hidden: true}, {Name: "environments", Hidden: true}, {Name: "cloud:env"}, + {Name: "upsun:env", Hidden: true}, {Name: "env", Hidden: true}, }, - Usage: "Get a list of environments", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.BoolFlag{Name: "no-inactive", Aliases: []string{"I"},}, - &console.BoolFlag{Name: "pipe",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "refresh",}, - &console.BoolFlag{Name: "reverse",}, - &console.StringFlag{Name: "sort", DefaultValue: "title",}, - &console.StringFlag{Name: "type",}, + Usage: "Get a list of environments", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.BoolFlag{Name: "no-inactive", Aliases: []string{"I"}}, + &console.BoolFlag{Name: "pipe"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "refresh"}, + &console.BoolFlag{Name: "reverse"}, + &console.StringFlag{Name: "sort", DefaultValue: "title"}, + &console.StringFlag{Name: "status"}, + &console.StringFlag{Name: "type"}, }, }, { Category: "cloud:environment", Name: "logs", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:logs", Hidden: true}, + {Name: "upsun:environment:logs", Hidden: true}, {Name: "cloud:log"}, + {Name: "upsun:log", Hidden: true}, {Name: "log", Hidden: true}, - {Name: "cloud:logs"}, - {Name: "logs", Hidden: true}, }, - Usage: "Read an environment's logs", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "lines",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "tail",}, - &console.StringFlag{Name: "worker",}, + Usage: "Read an environment's logs", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "instance", Aliases: []string{"I"}}, + &console.StringFlag{Name: "lines"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "tail"}, + &console.StringFlag{Name: "worker"}, }, }, { Category: "cloud:environment", Name: "merge", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:merge", Hidden: true}, + {Name: "upsun:environment:merge", Hidden: true}, {Name: "cloud:merge"}, + {Name: "upsun:merge", Hidden: true}, {Name: "merge", Hidden: true}, }, - Usage: "Merge an environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Merge an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, + }, + }, + { + Category: "cloud:environment", + Name: "pause", + Aliases: []*console.Alias{ + {Name: "environment:pause", Hidden: true}, + {Name: "upsun:environment:pause", Hidden: true}, + }, + Usage: "Pause an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "push", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:push", Hidden: true}, + {Name: "upsun:environment:push", Hidden: true}, {Name: "cloud:push"}, + {Name: "upsun:push", Hidden: true}, {Name: "push", Hidden: true}, {Name: "deploy"}, {Name: "cloud:deploy"}, + {Name: "upsun:deploy", Hidden: true}, }, - Usage: "Push code to an environment", - Flags: []console.Flag{ - &console.BoolFlag{Name: "activate",}, - &console.BoolFlag{Name: "branch",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.BoolFlag{Name: "force", Aliases: []string{"f"},}, - &console.BoolFlag{Name: "force-with-lease",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.BoolFlag{Name: "no-clone-parent",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "parent",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "set-upstream", Aliases: []string{"u"},}, - &console.StringFlag{Name: "target",}, - &console.StringFlag{Name: "type",}, - &console.BoolFlag{Name: "wait",}, + Usage: "Push code to an environment", + Flags: []console.Flag{ + &console.BoolFlag{Name: "activate"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "force", Aliases: []string{"f"}}, + &console.BoolFlag{Name: "force-with-lease"}, + &console.BoolFlag{Name: "no-clone-parent"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "parent"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "set-upstream", Aliases: []string{"u"}}, + &console.StringFlag{Name: "target"}, + &console.StringFlag{Name: "type"}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "redeploy", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:redeploy", Hidden: true}, + {Name: "upsun:environment:redeploy", Hidden: true}, {Name: "cloud:redeploy"}, + {Name: "upsun:redeploy", Hidden: true}, {Name: "redeploy", Hidden: true}, }, - Usage: "Redeploy an environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Redeploy an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "relationships", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:relationships", Hidden: true}, + {Name: "upsun:environment:relationships", Hidden: true}, {Name: "cloud:relationships"}, + {Name: "upsun:relationships", Hidden: true}, {Name: "relationships", Hidden: true}, + {Name: "cloud:rel"}, + {Name: "upsun:rel", Hidden: true}, + {Name: "rel", Hidden: true}, + }, + Usage: "Show an environment's relationships", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, + &console.BoolFlag{Name: "refresh"}, + }, + }, + { + Category: "cloud:environment", + Name: "resume", + Aliases: []*console.Alias{ + {Name: "environment:resume", Hidden: true}, + {Name: "upsun:environment:resume", Hidden: true}, }, - Usage: "Show an environment's relationships", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, - &console.BoolFlag{Name: "refresh",}, + Usage: "Resume a paused environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "scp", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:scp", Hidden: true}, + {Name: "upsun:environment:scp", Hidden: true}, {Name: "cloud:scp"}, + {Name: "upsun:scp", Hidden: true}, {Name: "scp", Hidden: true}, }, - Usage: "Copy files to and from current environment using scp", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "recursive", Aliases: []string{"r"},}, - &console.StringFlag{Name: "worker",}, + Usage: "Copy files to and from an environment using scp", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "instance", Aliases: []string{"I"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "recursive", Aliases: []string{"r"}}, + &console.StringFlag{Name: "worker"}, }, }, { Category: "cloud:environment", Name: "set-remote", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:set-remote", Hidden: true}, + {Name: "upsun:environment:set-remote", Hidden: true}, }, - Usage: "Set the remote environment to map to a branch", - Hidden: console.Hide, + Usage: "Set the remote environment to map to a branch", + Hidden: console.Hide, }, { Category: "cloud:environment", Name: "ssh", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:ssh", Hidden: true}, + {Name: "upsun:environment:ssh", Hidden: true}, {Name: "cloud:ssh"}, + {Name: "upsun:ssh", Hidden: true}, {Name: "ssh", Hidden: true}, }, - Usage: "SSH to the current environment", - Flags: []console.Flag{ - &console.BoolFlag{Name: "all",}, - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.BoolFlag{Name: "pipe",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "worker",}, + Usage: "SSH to the current environment", + Flags: []console.Flag{ + &console.BoolFlag{Name: "all"}, + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "instance", Aliases: []string{"I"}}, + &console.StringFlag{Name: "option", Aliases: []string{"o"}}, + &console.BoolFlag{Name: "pipe"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "worker"}, }, }, { Category: "cloud:environment", Name: "synchronize", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:synchronize", Hidden: true}, + {Name: "upsun:environment:synchronize", Hidden: true}, {Name: "cloud:sync"}, + {Name: "upsun:sync", Hidden: true}, {Name: "sync", Hidden: true}, }, - Usage: "Synchronize an environment's code and/or data from its parent", - Flags: []console.Flag{ - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "rebase",}, - &console.BoolFlag{Name: "wait",}, + Usage: "Synchronize an environment's code and/or data from its parent", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "rebase"}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:environment", Name: "url", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:url", Hidden: true}, + {Name: "upsun:environment:url", Hidden: true}, {Name: "cloud:url"}, + {Name: "upsun:url", Hidden: true}, {Name: "url", Hidden: true}, }, - Usage: "Get the public URLs of an environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "browser",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "pipe",}, - &console.BoolFlag{Name: "primary", Aliases: []string{"1"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "Get the public URLs of an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "browser"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "pipe"}, + &console.BoolFlag{Name: "primary", Aliases: []string{"1"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:environment", Name: "xdebug", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "environment:xdebug", Hidden: true}, + {Name: "upsun:environment:xdebug", Hidden: true}, {Name: "cloud:xdebug"}, + {Name: "upsun:xdebug", Hidden: true}, {Name: "xdebug", Hidden: true}, }, - Usage: "Open a tunnel to Xdebug on the environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "port",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "worker",}, + Usage: "Open a tunnel to Xdebug on the environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "instance", Aliases: []string{"I"}}, + &console.StringFlag{Name: "port"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "worker"}, }, }, { Category: "cloud:integration", Name: "activity:get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "integration:activity:get", Hidden: true}, + {Name: "upsun:integration:activity:get", Hidden: true}, }, - Usage: "View detailed information on a single integration activity", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, + Usage: "View detailed information on a single integration activity", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, }, }, { Category: "cloud:integration", Name: "activity:list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "integration:activity:list", Hidden: true}, - {Name: "cloud:i:act"}, - {Name: "i:act", Hidden: true}, + {Name: "upsun:integration:activity:list", Hidden: true}, {Name: "cloud:integration:activities"}, + {Name: "upsun:integration:activities", Hidden: true}, {Name: "integration:activities", Hidden: true}, }, - Usage: "Get a list of activities for an integration", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "exclude-type", Aliases: []string{"x"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "incomplete", Aliases: []string{"i"},}, - &console.StringFlag{Name: "limit",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "result",}, - &console.StringFlag{Name: "start",}, - &console.StringFlag{Name: "state",}, - &console.StringFlag{Name: "type",}, + Usage: "Get a list of activities for an integration", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "exclude-type", Aliases: []string{"x"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "incomplete", Aliases: []string{"i"}}, + &console.StringFlag{Name: "limit"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "result"}, + &console.StringFlag{Name: "start"}, + &console.StringFlag{Name: "state"}, + &console.StringFlag{Name: "type"}, }, }, { Category: "cloud:integration", Name: "activity:log", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "integration:activity:log", Hidden: true}, + {Name: "upsun:integration:activity:log", Hidden: true}, }, - Usage: "Display the log for an integration activity", - Flags: []console.Flag{ - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "timestamps", Aliases: []string{"t"},}, + Usage: "Display the log for an integration activity", + Flags: []console.Flag{ + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "timestamps", Aliases: []string{"t"}}, }, }, { Category: "cloud:integration", Name: "add", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "integration:add", Hidden: true}, - }, - Usage: "Add an integration to the project", - Flags: []console.Flag{ - &console.StringFlag{Name: "base-url",}, - &console.BoolFlag{Name: "build-draft-pull-requests", DefaultValue: true,}, - &console.BoolFlag{Name: "build-merge-requests", DefaultValue: true,}, - &console.BoolFlag{Name: "build-pull-requests", DefaultValue: true,}, - &console.BoolFlag{Name: "build-pull-requests-post-merge",}, - &console.BoolFlag{Name: "build-wip-merge-requests", DefaultValue: true,}, - &console.StringFlag{Name: "channel",}, - &console.StringFlag{Name: "environments",}, - &console.StringFlag{Name: "events",}, - &console.StringFlag{Name: "excluded-environments",}, - &console.BoolFlag{Name: "fetch-branches", DefaultValue: true,}, - &console.StringFlag{Name: "file",}, - &console.StringFlag{Name: "from-address",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "key",}, - &console.BoolFlag{Name: "merge-requests-clone-parent-data", DefaultValue: true,}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "prune-branches", DefaultValue: true,}, - &console.BoolFlag{Name: "pull-requests-clone-parent-data", DefaultValue: true,}, - &console.StringFlag{Name: "recipients",}, - &console.StringFlag{Name: "repository",}, - &console.BoolFlag{Name: "resync-pull-requests",}, - &console.StringFlag{Name: "routing-key",}, - &console.StringFlag{Name: "secret",}, - &console.StringFlag{Name: "server-project",}, - &console.StringFlag{Name: "shared-key",}, - &console.StringFlag{Name: "states",}, - &console.StringFlag{Name: "token",}, - &console.StringFlag{Name: "type",}, - &console.StringFlag{Name: "url",}, - &console.StringFlag{Name: "username",}, - &console.BoolFlag{Name: "wait",}, + {Name: "upsun:integration:add", Hidden: true}, + }, + Usage: "Add an integration to the project", + Flags: []console.Flag{ + &console.StringFlag{Name: "auth-mode", DefaultValue: "prefix"}, + &console.StringFlag{Name: "auth-token"}, + &console.StringFlag{Name: "base-url"}, + &console.StringFlag{Name: "bitbucket-url"}, + &console.BoolFlag{Name: "build-draft-pull-requests", DefaultValue: true}, + &console.BoolFlag{Name: "build-merge-requests", DefaultValue: true}, + &console.BoolFlag{Name: "build-pull-requests", DefaultValue: true}, + &console.BoolFlag{Name: "build-pull-requests-post-merge"}, + &console.BoolFlag{Name: "build-wip-merge-requests", DefaultValue: true}, + &console.StringFlag{Name: "category"}, + &console.StringFlag{Name: "channel"}, + &console.StringFlag{Name: "environments"}, + &console.StringFlag{Name: "events"}, + &console.StringFlag{Name: "excluded-environments"}, + &console.StringFlag{Name: "facility"}, + &console.BoolFlag{Name: "fetch-branches", DefaultValue: true}, + &console.StringFlag{Name: "file"}, + &console.StringFlag{Name: "from-address"}, + &console.StringFlag{Name: "header"}, + &console.StringFlag{Name: "index"}, + &console.StringFlag{Name: "key"}, + &console.StringFlag{Name: "license-key"}, + &console.BoolFlag{Name: "merge-requests-clone-parent-data", DefaultValue: true}, + &console.StringFlag{Name: "message-format", DefaultValue: "rfc5424"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "protocol", DefaultValue: "tls"}, + &console.BoolFlag{Name: "prune-branches", DefaultValue: true}, + &console.BoolFlag{Name: "pull-requests-clone-parent-data", DefaultValue: true}, + &console.StringFlag{Name: "recipients"}, + &console.StringFlag{Name: "repository"}, + &console.StringFlag{Name: "resources-init", DefaultValue: "parent"}, + &console.BoolFlag{Name: "resync-pull-requests"}, + &console.StringFlag{Name: "routing-key"}, + &console.StringFlag{Name: "secret"}, + &console.StringFlag{Name: "server-project"}, + &console.StringFlag{Name: "shared-key"}, + &console.StringFlag{Name: "sourcetype"}, + &console.StringFlag{Name: "states"}, + &console.StringFlag{Name: "syslog-host"}, + &console.StringFlag{Name: "syslog-port"}, + &console.StringFlag{Name: "token"}, + &console.StringFlag{Name: "type"}, + &console.StringFlag{Name: "url"}, + &console.StringFlag{Name: "username"}, + &console.BoolFlag{Name: "verify-tls", DefaultValue: true}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:integration", Name: "delete", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "integration:delete", Hidden: true}, + {Name: "upsun:integration:delete", Hidden: true}, }, - Usage: "Delete an integration from a project", - Flags: []console.Flag{ - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Delete an integration from a project", + Flags: []console.Flag{ + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:integration", Name: "get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "integration:get", Hidden: true}, + {Name: "upsun:integration:get", Hidden: true}, }, - Usage: "View details of an integration", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, + Usage: "View details of an integration", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, }, }, { Category: "cloud:integration", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "integration:list", Hidden: true}, + {Name: "upsun:integration:list", Hidden: true}, {Name: "cloud:integrations"}, + {Name: "upsun:integrations", Hidden: true}, {Name: "integrations", Hidden: true}, }, - Usage: "View a list of project integration(s)", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "View a list of project integration(s)", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "type", Aliases: []string{"t"}}, }, }, { Category: "cloud:integration", Name: "update", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "integration:update", Hidden: true}, - }, - Usage: "Update an integration", - Flags: []console.Flag{ - &console.StringFlag{Name: "base-url",}, - &console.BoolFlag{Name: "build-draft-pull-requests", DefaultValue: true,}, - &console.BoolFlag{Name: "build-merge-requests", DefaultValue: true,}, - &console.BoolFlag{Name: "build-pull-requests", DefaultValue: true,}, - &console.BoolFlag{Name: "build-pull-requests-post-merge",}, - &console.BoolFlag{Name: "build-wip-merge-requests", DefaultValue: true,}, - &console.StringFlag{Name: "channel",}, - &console.StringFlag{Name: "environments",}, - &console.StringFlag{Name: "events",}, - &console.StringFlag{Name: "excluded-environments",}, - &console.BoolFlag{Name: "fetch-branches", DefaultValue: true,}, - &console.StringFlag{Name: "file",}, - &console.StringFlag{Name: "from-address",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "key",}, - &console.BoolFlag{Name: "merge-requests-clone-parent-data", DefaultValue: true,}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "prune-branches", DefaultValue: true,}, - &console.BoolFlag{Name: "pull-requests-clone-parent-data", DefaultValue: true,}, - &console.StringFlag{Name: "recipients",}, - &console.StringFlag{Name: "repository",}, - &console.BoolFlag{Name: "resync-pull-requests",}, - &console.StringFlag{Name: "routing-key",}, - &console.StringFlag{Name: "secret",}, - &console.StringFlag{Name: "server-project",}, - &console.StringFlag{Name: "shared-key",}, - &console.StringFlag{Name: "states",}, - &console.StringFlag{Name: "token",}, - &console.StringFlag{Name: "type",}, - &console.StringFlag{Name: "url",}, - &console.StringFlag{Name: "username",}, - &console.BoolFlag{Name: "wait",}, + {Name: "upsun:integration:update", Hidden: true}, + }, + Usage: "Update an integration", + Flags: []console.Flag{ + &console.StringFlag{Name: "auth-mode", DefaultValue: "prefix"}, + &console.StringFlag{Name: "auth-token"}, + &console.StringFlag{Name: "base-url"}, + &console.StringFlag{Name: "bitbucket-url"}, + &console.BoolFlag{Name: "build-draft-pull-requests", DefaultValue: true}, + &console.BoolFlag{Name: "build-merge-requests", DefaultValue: true}, + &console.BoolFlag{Name: "build-pull-requests", DefaultValue: true}, + &console.BoolFlag{Name: "build-pull-requests-post-merge"}, + &console.BoolFlag{Name: "build-wip-merge-requests", DefaultValue: true}, + &console.StringFlag{Name: "category"}, + &console.StringFlag{Name: "channel"}, + &console.StringFlag{Name: "environments"}, + &console.StringFlag{Name: "events"}, + &console.StringFlag{Name: "excluded-environments"}, + &console.StringFlag{Name: "facility"}, + &console.BoolFlag{Name: "fetch-branches", DefaultValue: true}, + &console.StringFlag{Name: "file"}, + &console.StringFlag{Name: "from-address"}, + &console.StringFlag{Name: "header"}, + &console.StringFlag{Name: "index"}, + &console.StringFlag{Name: "key"}, + &console.StringFlag{Name: "license-key"}, + &console.BoolFlag{Name: "merge-requests-clone-parent-data", DefaultValue: true}, + &console.StringFlag{Name: "message-format", DefaultValue: "rfc5424"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "protocol", DefaultValue: "tls"}, + &console.BoolFlag{Name: "prune-branches", DefaultValue: true}, + &console.BoolFlag{Name: "pull-requests-clone-parent-data", DefaultValue: true}, + &console.StringFlag{Name: "recipients"}, + &console.StringFlag{Name: "repository"}, + &console.StringFlag{Name: "resources-init", DefaultValue: "parent"}, + &console.BoolFlag{Name: "resync-pull-requests"}, + &console.StringFlag{Name: "routing-key"}, + &console.StringFlag{Name: "secret"}, + &console.StringFlag{Name: "server-project"}, + &console.StringFlag{Name: "shared-key"}, + &console.StringFlag{Name: "sourcetype"}, + &console.StringFlag{Name: "states"}, + &console.StringFlag{Name: "syslog-host"}, + &console.StringFlag{Name: "syslog-port"}, + &console.StringFlag{Name: "token"}, + &console.StringFlag{Name: "type"}, + &console.StringFlag{Name: "url"}, + &console.StringFlag{Name: "username"}, + &console.BoolFlag{Name: "verify-tls", DefaultValue: true}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:integration", Name: "validate", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "integration:validate", Hidden: true}, - }, - Usage: "Validate an existing integration", - Flags: []console.Flag{ - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + {Name: "upsun:integration:validate", Hidden: true}, + }, + Usage: "Validate an existing integration", + Flags: []console.Flag{ + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + }, + }, + { + Category: "cloud:metrics", + Name: "all", + Aliases: []*console.Alias{ + {Name: "metrics:all", Hidden: true}, + {Name: "upsun:metrics:all", Hidden: true}, + {Name: "cloud:metrics"}, + {Name: "upsun:metrics", Hidden: true}, + {Name: "metrics", Hidden: true}, + {Name: "cloud:met"}, + {Name: "upsun:met", Hidden: true}, + {Name: "met", Hidden: true}, + }, + Usage: "Show CPU, disk and memory metrics for an environment", + Flags: []console.Flag{ + &console.BoolFlag{Name: "bytes", Aliases: []string{"B"}}, + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.StringFlag{Name: "interval", Aliases: []string{"i"}}, + &console.BoolFlag{Name: "latest", Aliases: []string{"1"}}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "range", Aliases: []string{"r"}}, + &console.StringFlag{Name: "service", Aliases: []string{"s"}}, + &console.StringFlag{Name: "to"}, + &console.StringFlag{Name: "type"}, + }, + }, + { + Category: "cloud:metrics", + Name: "cpu", + Aliases: []*console.Alias{ + {Name: "metrics:cpu", Hidden: true}, + {Name: "upsun:metrics:cpu", Hidden: true}, + {Name: "cloud:cpu"}, + {Name: "upsun:cpu", Hidden: true}, + {Name: "cpu", Hidden: true}, + }, + Usage: "Show CPU usage of an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.StringFlag{Name: "interval", Aliases: []string{"i"}}, + &console.BoolFlag{Name: "latest", Aliases: []string{"1"}}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "range", Aliases: []string{"r"}}, + &console.StringFlag{Name: "service", Aliases: []string{"s"}}, + &console.StringFlag{Name: "to"}, + &console.StringFlag{Name: "type"}, + }, + }, + { + Category: "cloud:metrics", + Name: "curl", + Aliases: []*console.Alias{ + {Name: "metrics:curl", Hidden: true}, + {Name: "upsun:metrics:curl", Hidden: true}, + }, + Usage: "Run an authenticated cURL request on an environment's metrics API", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "data", Aliases: []string{"d"}}, + &console.BoolFlag{Name: "disable-compression"}, + &console.BoolFlag{Name: "enable-glob"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "fail", Aliases: []string{"f"}}, + &console.BoolFlag{Name: "head", Aliases: []string{"I"}}, + &console.StringFlag{Name: "header", Aliases: []string{"H"}}, + &console.BoolFlag{Name: "include", Aliases: []string{"i"}}, + &console.StringFlag{Name: "json"}, + &console.BoolFlag{Name: "no-retry-401"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "request", Aliases: []string{"X"}}, + }, + }, + { + Category: "cloud:metrics", + Name: "disk-usage", + Aliases: []*console.Alias{ + {Name: "metrics:disk-usage", Hidden: true}, + {Name: "upsun:metrics:disk-usage", Hidden: true}, + {Name: "cloud:disk"}, + {Name: "upsun:disk", Hidden: true}, + {Name: "disk", Hidden: true}, + }, + Usage: "Show disk usage of an environment", + Flags: []console.Flag{ + &console.BoolFlag{Name: "bytes", Aliases: []string{"B"}}, + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.StringFlag{Name: "interval", Aliases: []string{"i"}}, + &console.BoolFlag{Name: "latest", Aliases: []string{"1"}}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "range", Aliases: []string{"r"}}, + &console.StringFlag{Name: "service", Aliases: []string{"s"}}, + &console.BoolFlag{Name: "tmp"}, + &console.StringFlag{Name: "to"}, + &console.StringFlag{Name: "type"}, + }, + }, + { + Category: "cloud:metrics", + Name: "memory", + Aliases: []*console.Alias{ + {Name: "metrics:memory", Hidden: true}, + {Name: "upsun:metrics:memory", Hidden: true}, + {Name: "cloud:mem"}, + {Name: "upsun:mem", Hidden: true}, + {Name: "mem", Hidden: true}, + {Name: "cloud:memory"}, + {Name: "upsun:memory", Hidden: true}, + {Name: "memory", Hidden: true}, + }, + Usage: "Show memory usage of an environment", + Flags: []console.Flag{ + &console.BoolFlag{Name: "bytes", Aliases: []string{"B"}}, + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.StringFlag{Name: "interval", Aliases: []string{"i"}}, + &console.BoolFlag{Name: "latest", Aliases: []string{"1"}}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "range", Aliases: []string{"r"}}, + &console.StringFlag{Name: "service", Aliases: []string{"s"}}, + &console.StringFlag{Name: "to"}, + &console.StringFlag{Name: "type"}, }, }, { Category: "cloud:mount", Name: "download", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "mount:download", Hidden: true}, - }, - Usage: "Download files from a mount, using rsync", - Flags: []console.Flag{ - &console.BoolFlag{Name: "all", Aliases: []string{"a"},}, - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.BoolFlag{Name: "delete",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "exclude",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "include",}, - &console.StringFlag{Name: "mount", Aliases: []string{"m"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "refresh",}, - &console.BoolFlag{Name: "source-path",}, - &console.StringFlag{Name: "target",}, - &console.StringFlag{Name: "worker",}, + {Name: "upsun:mount:download", Hidden: true}, + }, + Usage: "Download files from a mount, using rsync", + Flags: []console.Flag{ + &console.BoolFlag{Name: "all", Aliases: []string{"a"}}, + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.BoolFlag{Name: "delete"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "exclude"}, + &console.StringFlag{Name: "include"}, + &console.StringFlag{Name: "instance", Aliases: []string{"I"}}, + &console.StringFlag{Name: "mount", Aliases: []string{"m"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "refresh"}, + &console.BoolFlag{Name: "source-path"}, + &console.StringFlag{Name: "target"}, + &console.StringFlag{Name: "worker"}, }, }, { Category: "cloud:mount", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "mount:list", Hidden: true}, + {Name: "upsun:mount:list", Hidden: true}, {Name: "cloud:mounts"}, + {Name: "upsun:mounts", Hidden: true}, {Name: "mounts", Hidden: true}, }, - Usage: "Get a list of mounts", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.BoolFlag{Name: "paths",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "refresh",}, - &console.StringFlag{Name: "worker",}, + Usage: "Get a list of mounts", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.StringFlag{Name: "instance", Aliases: []string{"I"}}, + &console.BoolFlag{Name: "no-header"}, + &console.BoolFlag{Name: "paths"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "refresh"}, + &console.StringFlag{Name: "worker"}, }, }, { Category: "cloud:mount", - Name: "size", - Aliases: []*console.Alias{ - {Name: "mount:size", Hidden: true}, - }, - Usage: "Check the disk usage of mounts", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.BoolFlag{Name: "bytes", Aliases: []string{"B"},}, - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "refresh",}, - &console.StringFlag{Name: "worker",}, + Name: "upload", + Aliases: []*console.Alias{ + {Name: "mount:upload", Hidden: true}, + {Name: "upsun:mount:upload", Hidden: true}, + }, + Usage: "Upload files to a mount, using rsync", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.BoolFlag{Name: "delete"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "exclude"}, + &console.StringFlag{Name: "include"}, + &console.StringFlag{Name: "instance", Aliases: []string{"I"}}, + &console.StringFlag{Name: "mount", Aliases: []string{"m"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "refresh"}, + &console.StringFlag{Name: "source"}, + &console.StringFlag{Name: "worker"}, }, }, { - Category: "cloud:mount", - Name: "upload", - Aliases: []*console.Alias{ - {Name: "mount:upload", Hidden: true}, + Category: "cloud:operation", + Name: "list", + Aliases: []*console.Alias{ + {Name: "operation:list", Hidden: true}, + {Name: "upsun:operation:list", Hidden: true}, + {Name: "cloud:ops"}, + {Name: "upsun:ops", Hidden: true}, + {Name: "ops", Hidden: true}, + }, + Usage: "List runtime operations on an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "full"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "worker"}, + }, + }, + { + Category: "cloud:operation", + Name: "run", + Aliases: []*console.Alias{ + {Name: "operation:run", Hidden: true}, + {Name: "upsun:operation:run", Hidden: true}, }, - Usage: "Upload files to a mount, using rsync", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.BoolFlag{Name: "delete",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "exclude",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "include",}, - &console.StringFlag{Name: "mount", Aliases: []string{"m"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "refresh",}, - &console.StringFlag{Name: "source",}, - &console.StringFlag{Name: "worker",}, + Usage: "Run an operation on the environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, + &console.StringFlag{Name: "worker"}, }, }, { Category: "cloud:organization", Name: "billing:address", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:billing:address", Hidden: true}, + {Name: "upsun:organization:billing:address", Hidden: true}, }, - Usage: "View or change an organization's billing address", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, + Usage: "View or change an organization's billing address", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:organization", Name: "billing:profile", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:billing:profile", Hidden: true}, + {Name: "upsun:organization:billing:profile", Hidden: true}, }, - Usage: "View or change an organization's billing profile", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, + Usage: "View or change an organization's billing profile", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:organization", Name: "create", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:create", Hidden: true}, + {Name: "upsun:organization:create", Hidden: true}, }, - Usage: "Create a new organization", - Flags: []console.Flag{ - &console.StringFlag{Name: "country",}, - &console.StringFlag{Name: "label",}, - &console.StringFlag{Name: "name",}, + Usage: "Create a new organization", + Flags: []console.Flag{ + &console.StringFlag{Name: "country"}, + &console.StringFlag{Name: "label"}, + &console.StringFlag{Name: "name"}, }, }, { Category: "cloud:organization", Name: "curl", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:curl", Hidden: true}, - }, - Usage: "Run an authenticated cURL request on an organization's API", - Hidden: console.Hide, - Flags: []console.Flag{ - &console.StringFlag{Name: "data", Aliases: []string{"d"},}, - &console.BoolFlag{Name: "disable-compression",}, - &console.BoolFlag{Name: "enable-glob",}, - &console.BoolFlag{Name: "fail", Aliases: []string{"f"},}, - &console.BoolFlag{Name: "head", Aliases: []string{"I"},}, - &console.StringFlag{Name: "header", Aliases: []string{"H"},}, - &console.BoolFlag{Name: "include", Aliases: []string{"i"},}, - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, - &console.StringFlag{Name: "request", Aliases: []string{"X"},}, + {Name: "upsun:organization:curl", Hidden: true}, + }, + Usage: "Run an authenticated cURL request on an organization's API", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "data", Aliases: []string{"d"}}, + &console.BoolFlag{Name: "disable-compression"}, + &console.BoolFlag{Name: "enable-glob"}, + &console.BoolFlag{Name: "fail", Aliases: []string{"f"}}, + &console.BoolFlag{Name: "head", Aliases: []string{"I"}}, + &console.StringFlag{Name: "header", Aliases: []string{"H"}}, + &console.BoolFlag{Name: "include", Aliases: []string{"i"}}, + &console.StringFlag{Name: "json"}, + &console.BoolFlag{Name: "no-retry-401"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "request", Aliases: []string{"X"}}, }, }, { Category: "cloud:organization", Name: "delete", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:delete", Hidden: true}, + {Name: "upsun:organization:delete", Hidden: true}, }, - Usage: "Delete an organization", - Flags: []console.Flag{ - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, + Usage: "Delete an organization", + Flags: []console.Flag{ + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:organization", Name: "info", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:info", Hidden: true}, + {Name: "upsun:organization:info", Hidden: true}, }, - Usage: "View or change organization details", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, + Usage: "View or change organization details", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "refresh"}, }, }, { Category: "cloud:organization", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:list", Hidden: true}, + {Name: "upsun:organization:list", Hidden: true}, {Name: "cloud:orgs"}, + {Name: "upsun:orgs", Hidden: true}, {Name: "orgs", Hidden: true}, {Name: "cloud:organizations"}, + {Name: "upsun:organizations", Hidden: true}, {Name: "organizations", Hidden: true}, }, - Usage: "List organizations", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.BoolFlag{Name: "my",}, - &console.BoolFlag{Name: "no-header",}, - &console.BoolFlag{Name: "reverse",}, - &console.StringFlag{Name: "sort",}, + Usage: "List organizations", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "my"}, + &console.BoolFlag{Name: "no-header"}, + &console.BoolFlag{Name: "reverse"}, + &console.StringFlag{Name: "sort"}, }, }, { Category: "cloud:organization", Name: "subscription:list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:subscription:list", Hidden: true}, - {Name: "cloud:organization:subscriptions"}, - {Name: "organization:subscriptions", Hidden: true}, + {Name: "upsun:organization:subscription:list", Hidden: true}, + {Name: "cloud:org:subs"}, + {Name: "upsun:org:subs", Hidden: true}, + {Name: "org:subs", Hidden: true}, }, - Usage: "List subscriptions within an organization", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, + Usage: "List subscriptions within an organization", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns"}, + &console.StringFlag{Name: "count", Aliases: []string{"c"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "page"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:organization", Name: "user:add", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:user:add", Hidden: true}, + {Name: "upsun:organization:user:add", Hidden: true}, }, - Usage: "Invite a user to an organization", - Flags: []console.Flag{ - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, - &console.StringFlag{Name: "permission",}, + Usage: "Invite a user to an organization", + Flags: []console.Flag{ + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "permission"}, }, }, { Category: "cloud:organization", Name: "user:delete", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:user:delete", Hidden: true}, + {Name: "upsun:organization:user:delete", Hidden: true}, }, - Usage: "Remove a user from an organization", - Flags: []console.Flag{ - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, + Usage: "Remove a user from an organization", + Flags: []console.Flag{ + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, }, }, { Category: "cloud:organization", Name: "user:get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:user:get", Hidden: true}, + {Name: "upsun:organization:user:get", Hidden: true}, }, - Usage: "View an organization user", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, + Usage: "View an organization user", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, }, }, { Category: "cloud:organization", Name: "user:list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:user:list", Hidden: true}, - {Name: "cloud:organization:users"}, - {Name: "organization:users", Hidden: true}, + {Name: "upsun:organization:user:list", Hidden: true}, + {Name: "cloud:org:users"}, + {Name: "upsun:org:users", Hidden: true}, + {Name: "org:users", Hidden: true}, + }, + Usage: "List organization users", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns"}, + &console.StringFlag{Name: "count", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.BoolFlag{Name: "reverse"}, + &console.StringFlag{Name: "sort", DefaultValue: "created_at"}, + }, + }, + { + Category: "cloud:organization", + Name: "user:projects", + Aliases: []*console.Alias{ + {Name: "organization:user:projects", Hidden: true}, + {Name: "upsun:organization:user:projects", Hidden: true}, + {Name: "cloud:oups"}, + {Name: "upsun:oups", Hidden: true}, + {Name: "oups", Hidden: true}, }, - Usage: "List organization users", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, + Usage: "List the projects a user can access", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "list-all"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, }, }, { Category: "cloud:organization", Name: "user:update", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "organization:user:update", Hidden: true}, + {Name: "upsun:organization:user:update", Hidden: true}, }, - Usage: "Update an organization user", - Flags: []console.Flag{ - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, - &console.StringFlag{Name: "permission",}, + Usage: "Update an organization user", + Flags: []console.Flag{ + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "permission"}, }, }, { Category: "cloud:project", Name: "clear-build-cache", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "project:clear-build-cache", Hidden: true}, + {Name: "upsun:project:clear-build-cache", Hidden: true}, }, - Usage: "Clear a project's build cache", - Flags: []console.Flag{ - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "Clear a project's build cache", + Flags: []console.Flag{ + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:project", Name: "create", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "project:create", Hidden: true}, + {Name: "upsun:project:create", Hidden: true}, {Name: "cloud:create"}, + {Name: "upsun:create", Hidden: true}, {Name: "create", Hidden: true}, }, - Usage: "Create a new project", - Flags: []console.Flag{ - &console.StringFlag{Name: "check-timeout",}, - &console.StringFlag{Name: "default-branch", DefaultValue: "main",}, - &console.StringFlag{Name: "environments",}, - &console.BoolFlag{Name: "no-set-remote",}, - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, - &console.StringFlag{Name: "plan", DefaultValue: "development",}, - &console.StringFlag{Name: "region",}, - &console.BoolFlag{Name: "set-remote",}, - &console.StringFlag{Name: "storage",}, - &console.StringFlag{Name: "timeout",}, - &console.StringFlag{Name: "title", DefaultValue: "Untitled Project",}, + Usage: "Create a new project", + Flags: []console.Flag{ + &console.StringFlag{Name: "default-branch", DefaultValue: "main"}, + &console.StringFlag{Name: "environments"}, + &console.StringFlag{Name: "init-repo"}, + &console.BoolFlag{Name: "no-set-remote"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "plan"}, + &console.StringFlag{Name: "region"}, + &console.BoolFlag{Name: "set-remote"}, + &console.StringFlag{Name: "storage"}, + &console.StringFlag{Name: "title", DefaultValue: "Untitled Project"}, }, }, { Category: "cloud:project", Name: "curl", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "project:curl", Hidden: true}, + {Name: "upsun:project:curl", Hidden: true}, }, - Usage: "Run an authenticated cURL request on a project's API", - Hidden: console.Hide, - Flags: []console.Flag{ - &console.StringFlag{Name: "data", Aliases: []string{"d"},}, - &console.BoolFlag{Name: "disable-compression",}, - &console.BoolFlag{Name: "enable-glob",}, - &console.BoolFlag{Name: "fail", Aliases: []string{"f"},}, - &console.BoolFlag{Name: "head", Aliases: []string{"I"},}, - &console.StringFlag{Name: "header", Aliases: []string{"H"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "include", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "request", Aliases: []string{"X"},}, + Usage: "Run an authenticated cURL request on a project's API", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "data", Aliases: []string{"d"}}, + &console.BoolFlag{Name: "disable-compression"}, + &console.BoolFlag{Name: "enable-glob"}, + &console.BoolFlag{Name: "fail", Aliases: []string{"f"}}, + &console.BoolFlag{Name: "head", Aliases: []string{"I"}}, + &console.StringFlag{Name: "header", Aliases: []string{"H"}}, + &console.BoolFlag{Name: "include", Aliases: []string{"i"}}, + &console.StringFlag{Name: "json"}, + &console.BoolFlag{Name: "no-retry-401"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "request", Aliases: []string{"X"}}, }, }, { Category: "cloud:project", Name: "delete", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "project:delete", Hidden: true}, + {Name: "upsun:project:delete", Hidden: true}, }, - Usage: "Delete a project", - Flags: []console.Flag{ - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "Delete a project", + Flags: []console.Flag{ + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:project", Name: "get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "project:get", Hidden: true}, + {Name: "upsun:project:get", Hidden: true}, {Name: "cloud:get"}, + {Name: "upsun:get", Hidden: true}, {Name: "get", Hidden: true}, }, - Usage: "Clone a project locally", - Flags: []console.Flag{ - &console.BoolFlag{Name: "build",}, - &console.StringFlag{Name: "depth",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "Clone a project locally", + Flags: []console.Flag{ + &console.BoolFlag{Name: "build"}, + &console.StringFlag{Name: "depth"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:project", Name: "info", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "project:info", Hidden: true}, - {Name: "cloud:project:metadata"}, - {Name: "project:metadata", Hidden: true}, + {Name: "upsun:project:info", Hidden: true}, }, - Usage: "Read or set properties for a project", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "refresh",}, - &console.BoolFlag{Name: "wait",}, + Usage: "Read or set properties for a project", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "refresh"}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:project", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "project:list", Hidden: true}, + {Name: "upsun:project:list", Hidden: true}, {Name: "cloud:projects"}, + {Name: "upsun:projects", Hidden: true}, {Name: "projects", Hidden: true}, {Name: "cloud:pro"}, + {Name: "upsun:pro", Hidden: true}, {Name: "pro", Hidden: true}, }, - Usage: "Get a list of all active projects", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "count",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "my",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "org", Aliases: []string{"o"},}, - &console.StringFlag{Name: "page", DefaultValue: "1",}, - &console.BoolFlag{Name: "pipe",}, - &console.StringFlag{Name: "refresh",}, - &console.BoolFlag{Name: "reverse",}, - &console.StringFlag{Name: "sort", DefaultValue: "title",}, - &console.StringFlag{Name: "title",}, + Usage: "Get a list of all active projects", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns"}, + &console.StringFlag{Name: "count", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "my"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "page"}, + &console.BoolFlag{Name: "pipe"}, + &console.StringFlag{Name: "refresh"}, + &console.StringFlag{Name: "region"}, + &console.BoolFlag{Name: "reverse"}, + &console.StringFlag{Name: "sort", DefaultValue: "title"}, + &console.StringFlag{Name: "title"}, }, }, { Category: "cloud:project", Name: "set-remote", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "project:set-remote", Hidden: true}, + {Name: "upsun:project:set-remote", Hidden: true}, + {Name: "cloud:set-remote"}, + {Name: "upsun:set-remote", Hidden: true}, + {Name: "set-remote", Hidden: true}, }, - Usage: "Set the remote project for the current Git repository", + Usage: "Set the remote project for the current Git repository", }, { Category: "cloud:repo", Name: "cat", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "repo:cat", Hidden: true}, + {Name: "upsun:repo:cat", Hidden: true}, }, - Usage: "Read a file in the project repository", - Flags: []console.Flag{ - &console.StringFlag{Name: "commit", Aliases: []string{"c"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "Read a file in the project repository", + Flags: []console.Flag{ + &console.StringFlag{Name: "commit", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:repo", Name: "ls", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "repo:ls", Hidden: true}, + {Name: "upsun:repo:ls", Hidden: true}, }, - Usage: "List files in the project repository", - Flags: []console.Flag{ - &console.StringFlag{Name: "commit", Aliases: []string{"c"},}, - &console.BoolFlag{Name: "directories", Aliases: []string{"d"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.BoolFlag{Name: "files", Aliases: []string{"f"},}, - &console.BoolFlag{Name: "git-style",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "List files in the project repository", + Flags: []console.Flag{ + &console.StringFlag{Name: "commit", Aliases: []string{"c"}}, + &console.BoolFlag{Name: "directories", Aliases: []string{"d"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "files", Aliases: []string{"f"}}, + &console.BoolFlag{Name: "git-style"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:repo", Name: "read", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "repo:read", Hidden: true}, + {Name: "upsun:repo:read", Hidden: true}, {Name: "cloud:read"}, + {Name: "upsun:read", Hidden: true}, {Name: "read", Hidden: true}, }, - Usage: "Read a directory or file in the project repository", - Flags: []console.Flag{ - &console.StringFlag{Name: "commit", Aliases: []string{"c"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "Read a directory or file in the project repository", + Flags: []console.Flag{ + &console.StringFlag{Name: "commit", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + }, + }, + { + Category: "cloud:resources", + Name: "build:get", + Aliases: []*console.Alias{ + {Name: "resources:build:get", Hidden: true}, + {Name: "upsun:resources:build:get", Hidden: true}, + {Name: "cloud:build-resources:get"}, + {Name: "upsun:build-resources:get", Hidden: true}, + {Name: "build-resources:get", Hidden: true}, + {Name: "cloud:build-resources"}, + {Name: "upsun:build-resources", Hidden: true}, + {Name: "build-resources", Hidden: true}, + }, + Usage: "View the build resources of a project", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + }, + }, + { + Category: "cloud:resources", + Name: "build:set", + Aliases: []*console.Alias{ + {Name: "resources:build:set", Hidden: true}, + {Name: "upsun:resources:build:set", Hidden: true}, + {Name: "cloud:build-resources:set"}, + {Name: "upsun:build-resources:set", Hidden: true}, + {Name: "build-resources:set", Hidden: true}, + }, + Usage: "Set the build resources of a project", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "cpu"}, + &console.StringFlag{Name: "memory"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + }, + }, + { + Category: "cloud:resources", + Name: "get", + Aliases: []*console.Alias{ + {Name: "resources:get", Hidden: true}, + {Name: "upsun:resources:get", Hidden: true}, + {Name: "cloud:resources"}, + {Name: "upsun:resources", Hidden: true}, + {Name: "resources", Hidden: true}, + {Name: "cloud:res"}, + {Name: "upsun:res", Hidden: true}, + {Name: "res", Hidden: true}, + }, + Usage: "View the resources of apps and services on an environment", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "app"}, + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "service", Aliases: []string{"s"}}, + &console.StringFlag{Name: "type"}, + &console.StringFlag{Name: "worker"}, + }, + }, + { + Category: "cloud:resources", + Name: "set", + Aliases: []*console.Alias{ + {Name: "resources:set", Hidden: true}, + {Name: "upsun:resources:set", Hidden: true}, + }, + Usage: "Set the resources of apps and services on an environment", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "count", Aliases: []string{"C"}}, + &console.StringFlag{Name: "disk", Aliases: []string{"D"}}, + &console.BoolFlag{Name: "dry-run"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "force", Aliases: []string{"f"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "size", Aliases: []string{"S"}}, + &console.BoolFlag{Name: "wait"}, + }, + }, + { + Category: "cloud:resources", + Name: "size:list", + Aliases: []*console.Alias{ + {Name: "resources:size:list", Hidden: true}, + {Name: "upsun:resources:size:list", Hidden: true}, + {Name: "cloud:resources:sizes"}, + {Name: "upsun:resources:sizes", Hidden: true}, + {Name: "resources:sizes", Hidden: true}, + }, + Usage: "List container profile sizes", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "profile"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "service", Aliases: []string{"s"}}, }, }, { Category: "cloud:route", Name: "get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "route:get", Hidden: true}, + {Name: "upsun:route:get", Hidden: true}, }, - Usage: "View detailed information about a route", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "id",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.BoolFlag{Name: "primary", Aliases: []string{"1"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, - &console.BoolFlag{Name: "refresh",}, + Usage: "View detailed information about a route", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "id"}, + &console.StringFlag{Name: "identity-file", Aliases: []string{"i"}}, + &console.BoolFlag{Name: "primary", Aliases: []string{"1"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, + &console.BoolFlag{Name: "refresh"}, }, }, { Category: "cloud:route", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "route:list", Hidden: true}, + {Name: "upsun:route:list", Hidden: true}, {Name: "cloud:routes"}, + {Name: "upsun:routes", Hidden: true}, {Name: "routes", Hidden: true}, - {Name: "cloud:environment:routes"}, - {Name: "environment:routes", Hidden: true}, }, - Usage: "List all routes for an environment", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "refresh",}, + Usage: "List all routes for an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "refresh"}, }, }, { Category: "cloud:self", - Name: "install", - Aliases: []*console.Alias{ - {Name: "cloud:local:install"}, - }, - Usage: "Install or update CLI configuration files", - Hidden: console.Hide, - Flags: []console.Flag{ - &console.StringFlag{Name: "shell-type",}, - }, - }, - { - Category: "cloud:self", - Name: "update", - Aliases: []*console.Alias{ - {Name: "cloud:self-update"}, - {Name: "cloud:update"}, - }, - Usage: "Update the CLI to the latest version", - Hidden: console.Hide, - Flags: []console.Flag{ - &console.StringFlag{Name: "current-version",}, - &console.StringFlag{Name: "manifest",}, - &console.BoolFlag{Name: "no-major",}, - &console.StringFlag{Name: "timeout",}, - &console.BoolFlag{Name: "unstable",}, + Name: "config", + Aliases: []*console.Alias{ + {Name: "upsun:self:config", Hidden: true}, }, + Usage: "Read CLI config", + Hidden: console.Hide, }, { Category: "cloud:service", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "service:list", Hidden: true}, + {Name: "upsun:service:list", Hidden: true}, {Name: "cloud:services"}, + {Name: "upsun:services", Hidden: true}, {Name: "services", Hidden: true}, }, - Usage: "List services in the project", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "refresh",}, + Usage: "List services in the project", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.BoolFlag{Name: "pipe"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "refresh"}, }, }, { Category: "cloud:service", Name: "mongo:dump", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "service:mongo:dump", Hidden: true}, + {Name: "upsun:service:mongo:dump", Hidden: true}, {Name: "cloud:mongodump"}, + {Name: "upsun:mongodump", Hidden: true}, {Name: "mongodump", Hidden: true}, }, - Usage: "Create a binary archive dump of data from MongoDB", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "collection", Aliases: []string{"c"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.BoolFlag{Name: "gzip", Aliases: []string{"z"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "relationship", Aliases: []string{"r"},}, - &console.BoolFlag{Name: "stdout", Aliases: []string{"o"},}, + Usage: "Create a binary archive dump of data from MongoDB", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "collection", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "gzip", Aliases: []string{"z"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "relationship", Aliases: []string{"r"}}, + &console.BoolFlag{Name: "stdout", Aliases: []string{"o"}}, }, }, { Category: "cloud:service", Name: "mongo:export", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "service:mongo:export", Hidden: true}, + {Name: "upsun:service:mongo:export", Hidden: true}, {Name: "cloud:mongoexport"}, + {Name: "upsun:mongoexport", Hidden: true}, {Name: "mongoexport", Hidden: true}, }, - Usage: "Export data from MongoDB", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "collection", Aliases: []string{"c"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "fields", Aliases: []string{"f"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.BoolFlag{Name: "jsonArray",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "relationship", Aliases: []string{"r"},}, - &console.StringFlag{Name: "type",}, + Usage: "Export data from MongoDB", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "collection", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "fields", Aliases: []string{"f"}}, + &console.BoolFlag{Name: "jsonArray"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "relationship", Aliases: []string{"r"}}, + &console.StringFlag{Name: "type"}, }, }, { Category: "cloud:service", Name: "mongo:restore", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "service:mongo:restore", Hidden: true}, + {Name: "upsun:service:mongo:restore", Hidden: true}, {Name: "cloud:mongorestore"}, + {Name: "upsun:mongorestore", Hidden: true}, {Name: "mongorestore", Hidden: true}, }, - Usage: "Restore a binary archive dump of data into MongoDB", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "collection", Aliases: []string{"c"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "relationship", Aliases: []string{"r"},}, + Usage: "Restore a binary archive dump of data into MongoDB", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "collection", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "relationship", Aliases: []string{"r"}}, }, }, { Category: "cloud:service", Name: "mongo:shell", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "service:mongo:shell", Hidden: true}, + {Name: "upsun:service:mongo:shell", Hidden: true}, {Name: "cloud:mongo"}, + {Name: "upsun:mongo", Hidden: true}, {Name: "mongo", Hidden: true}, }, - Usage: "Use the MongoDB shell", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "eval",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "relationship", Aliases: []string{"r"},}, + Usage: "Use the MongoDB shell", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "eval"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "relationship", Aliases: []string{"r"}}, }, }, { Category: "cloud:service", Name: "redis-cli", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "service:redis-cli", Hidden: true}, + {Name: "upsun:service:redis-cli", Hidden: true}, {Name: "cloud:redis"}, + {Name: "upsun:redis", Hidden: true}, {Name: "redis", Hidden: true}, }, - Usage: "Access the Redis CLI", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "relationship", Aliases: []string{"r"},}, + Usage: "Access the Redis CLI", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "relationship", Aliases: []string{"r"}}, + }, + }, + { + Category: "cloud:service", + Name: "valkey-cli", + Aliases: []*console.Alias{ + {Name: "service:valkey-cli", Hidden: true}, + {Name: "upsun:service:valkey-cli", Hidden: true}, + {Name: "cloud:valkey"}, + {Name: "upsun:valkey", Hidden: true}, + {Name: "valkey", Hidden: true}, + }, + Usage: "Access the Valkey CLI", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "relationship", Aliases: []string{"r"}}, }, }, { Category: "cloud:session", Name: "switch", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "session:switch", Hidden: true}, + {Name: "upsun:session:switch", Hidden: true}, + }, + Usage: "BETA Switch between sessions", + Hidden: console.Hide, + }, + { + Category: "cloud:source-operation", + Name: "list", + Aliases: []*console.Alias{ + {Name: "source-operation:list", Hidden: true}, + {Name: "upsun:source-operation:list", Hidden: true}, + {Name: "cloud:source-ops"}, + {Name: "upsun:source-ops", Hidden: true}, + {Name: "source-ops", Hidden: true}, + }, + Usage: "List source operations on an environment", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "full"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, - Usage: "[ BETA ] Switch between sessions", - Hidden: console.Hide, }, { Category: "cloud:source-operation", Name: "run", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "source-operation:run", Hidden: true}, + {Name: "upsun:source-operation:run", Hidden: true}, }, - Usage: "[ BETA ] Run a source operation", - Hidden: console.Hide, - Flags: []console.Flag{ - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "variable",}, - &console.BoolFlag{Name: "wait",}, + Usage: "Run a source operation", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "variable"}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:ssh-cert", Name: "info", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "ssh-cert:info", Hidden: true}, + {Name: "upsun:ssh-cert:info", Hidden: true}, }, - Usage: "Display information about the current SSH certificate", - Hidden: console.Hide, - Flags: []console.Flag{ - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.BoolFlag{Name: "no-refresh",}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, + Usage: "Display information about the current SSH certificate", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.BoolFlag{Name: "no-refresh"}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, }, }, { Category: "cloud:ssh-cert", Name: "load", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "ssh-cert:load", Hidden: true}, + {Name: "upsun:ssh-cert:load", Hidden: true}, }, - Usage: "Generate an SSH certificate", - Flags: []console.Flag{ - &console.BoolFlag{Name: "new",}, - &console.BoolFlag{Name: "new-key",}, - &console.BoolFlag{Name: "refresh-only",}, + Usage: "Generate an SSH certificate", + Flags: []console.Flag{ + &console.BoolFlag{Name: "new"}, + &console.BoolFlag{Name: "new-key"}, + &console.BoolFlag{Name: "refresh-only"}, }, }, { Category: "cloud:ssh-key", Name: "add", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "ssh-key:add", Hidden: true}, + {Name: "upsun:ssh-key:add", Hidden: true}, }, - Usage: "Add a new SSH key", - Flags: []console.Flag{ - &console.StringFlag{Name: "name",}, + Usage: "Add a new SSH key", + Flags: []console.Flag{ + &console.StringFlag{Name: "name"}, }, }, { Category: "cloud:ssh-key", Name: "delete", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "ssh-key:delete", Hidden: true}, + {Name: "upsun:ssh-key:delete", Hidden: true}, }, - Usage: "Delete an SSH key", + Usage: "Delete an SSH key", }, { Category: "cloud:ssh-key", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "ssh-key:list", Hidden: true}, + {Name: "upsun:ssh-key:list", Hidden: true}, {Name: "cloud:ssh-keys"}, + {Name: "upsun:ssh-keys", Hidden: true}, {Name: "ssh-keys", Hidden: true}, }, - Usage: "Get a list of SSH keys in your account", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.BoolFlag{Name: "no-header",}, + Usage: "Get a list of SSH keys in your account", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, }, }, { Category: "cloud:subscription", Name: "info", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "subscription:info", Hidden: true}, + {Name: "upsun:subscription:info", Hidden: true}, }, - Usage: "Read or modify subscription properties", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "date-fmt", DefaultValue: "c",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "id", Aliases: []string{"s"},}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "Read or modify subscription properties", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.StringFlag{Name: "id", Aliases: []string{"s"}}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + }, + }, + { + Category: "cloud:team", + Name: "create", + Aliases: []*console.Alias{ + {Name: "team:create", Hidden: true}, + {Name: "upsun:team:create", Hidden: true}, + }, + Usage: "Create a new team", + Flags: []console.Flag{ + &console.StringFlag{Name: "label"}, + &console.BoolFlag{Name: "no-check-unique"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.BoolFlag{Name: "output-id"}, + &console.StringFlag{Name: "role", Aliases: []string{"r"}}, + }, + }, + { + Category: "cloud:team", + Name: "delete", + Aliases: []*console.Alias{ + {Name: "team:delete", Hidden: true}, + {Name: "upsun:team:delete", Hidden: true}, + }, + Usage: "Delete a team", + Flags: []console.Flag{ + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "team", Aliases: []string{"t"}}, + }, + }, + { + Category: "cloud:team", + Name: "get", + Aliases: []*console.Alias{ + {Name: "team:get", Hidden: true}, + {Name: "upsun:team:get", Hidden: true}, + }, + Usage: "View a team", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, + &console.StringFlag{Name: "team", Aliases: []string{"t"}}, + }, + }, + { + Category: "cloud:team", + Name: "list", + Aliases: []*console.Alias{ + {Name: "team:list", Hidden: true}, + {Name: "upsun:team:list", Hidden: true}, + {Name: "cloud:teams"}, + {Name: "upsun:teams", Hidden: true}, + {Name: "teams", Hidden: true}, + }, + Usage: "List teams", + Flags: []console.Flag{ + &console.BoolFlag{Name: "all", Aliases: []string{"A"}}, + &console.StringFlag{Name: "columns"}, + &console.StringFlag{Name: "count", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "reverse"}, + &console.StringFlag{Name: "sort", DefaultValue: "label"}, + }, + }, + { + Category: "cloud:team", + Name: "project:add", + Aliases: []*console.Alias{ + {Name: "team:project:add", Hidden: true}, + {Name: "upsun:team:project:add", Hidden: true}, + }, + Usage: "Add project(s) to a team", + Flags: []console.Flag{ + &console.BoolFlag{Name: "all"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "team", Aliases: []string{"t"}}, + }, + }, + { + Category: "cloud:team", + Name: "project:delete", + Aliases: []*console.Alias{ + {Name: "team:project:delete", Hidden: true}, + {Name: "upsun:team:project:delete", Hidden: true}, + }, + Usage: "Remove a project from a team", + Flags: []console.Flag{ + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "team", Aliases: []string{"t"}}, + }, + }, + { + Category: "cloud:team", + Name: "project:list", + Aliases: []*console.Alias{ + {Name: "team:project:list", Hidden: true}, + {Name: "upsun:team:project:list", Hidden: true}, + {Name: "cloud:team:projects"}, + {Name: "upsun:team:projects", Hidden: true}, + {Name: "team:projects", Hidden: true}, + {Name: "cloud:team:pro"}, + {Name: "upsun:team:pro", Hidden: true}, + {Name: "team:pro", Hidden: true}, + }, + Usage: "List projects in a team", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns"}, + &console.StringFlag{Name: "count", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "team", Aliases: []string{"t"}}, + }, + }, + { + Category: "cloud:team", + Name: "update", + Aliases: []*console.Alias{ + {Name: "team:update", Hidden: true}, + {Name: "upsun:team:update", Hidden: true}, + }, + Usage: "Update a team", + Flags: []console.Flag{ + &console.StringFlag{Name: "label"}, + &console.BoolFlag{Name: "no-check-unique"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "role", Aliases: []string{"r"}}, + &console.StringFlag{Name: "team", Aliases: []string{"t"}}, + &console.BoolFlag{Name: "wait"}, + }, + }, + { + Category: "cloud:team", + Name: "user:add", + Aliases: []*console.Alias{ + {Name: "team:user:add", Hidden: true}, + {Name: "upsun:team:user:add", Hidden: true}, + }, + Usage: "Add a user to a team", + Flags: []console.Flag{ + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "team", Aliases: []string{"t"}}, + }, + }, + { + Category: "cloud:team", + Name: "user:delete", + Aliases: []*console.Alias{ + {Name: "team:user:delete", Hidden: true}, + {Name: "upsun:team:user:delete", Hidden: true}, + }, + Usage: "Remove a user from a team", + Flags: []console.Flag{ + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "team", Aliases: []string{"t"}}, + }, + }, + { + Category: "cloud:team", + Name: "user:list", + Aliases: []*console.Alias{ + {Name: "team:user:list", Hidden: true}, + {Name: "upsun:team:user:list", Hidden: true}, + {Name: "cloud:team:users"}, + {Name: "upsun:team:users", Hidden: true}, + {Name: "team:users", Hidden: true}, + }, + Usage: "List users in a team", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns"}, + &console.StringFlag{Name: "count", Aliases: []string{"c"}}, + &console.StringFlag{Name: "date-fmt", DefaultValue: "c"}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "org", Aliases: []string{"o"}}, + &console.StringFlag{Name: "team", Aliases: []string{"t"}}, }, }, { Category: "cloud:tunnel", Name: "close", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "tunnel:close", Hidden: true}, + {Name: "upsun:tunnel:close", Hidden: true}, }, - Usage: "Close SSH tunnels", - Flags: []console.Flag{ - &console.BoolFlag{Name: "all", Aliases: []string{"a"},}, - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "Close SSH tunnels", + Flags: []console.Flag{ + &console.BoolFlag{Name: "all", Aliases: []string{"a"}}, + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:tunnel", Name: "info", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "tunnel:info", Hidden: true}, + {Name: "upsun:tunnel:info", Hidden: true}, }, - Usage: "View relationship info for SSH tunnels", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "columns",}, - &console.BoolFlag{Name: "encode", Aliases: []string{"c"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, + Usage: "View relationship info for SSH tunnels", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.BoolFlag{Name: "encode", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, }, }, { Category: "cloud:tunnel", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "tunnel:list", Hidden: true}, + {Name: "upsun:tunnel:list", Hidden: true}, {Name: "cloud:tunnels"}, + {Name: "upsun:tunnels", Hidden: true}, {Name: "tunnels", Hidden: true}, }, - Usage: "List SSH tunnels", - Flags: []console.Flag{ - &console.BoolFlag{Name: "all", Aliases: []string{"a"},}, - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "List SSH tunnels", + Flags: []console.Flag{ + &console.BoolFlag{Name: "all", Aliases: []string{"a"}}, + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:tunnel", Name: "open", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "tunnel:open", Hidden: true}, + {Name: "upsun:tunnel:open", Hidden: true}, }, - Usage: "Open SSH tunnels to an app's relationships", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.BoolFlag{Name: "gateway-ports", Aliases: []string{"g"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "Open SSH tunnels to an app's relationships", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "gateway-ports", Aliases: []string{"g"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:tunnel", Name: "single", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "tunnel:single", Hidden: true}, + {Name: "upsun:tunnel:single", Hidden: true}, }, - Usage: "Open a single SSH tunnel to an app relationship", - Flags: []console.Flag{ - &console.StringFlag{Name: "app", Aliases: []string{"A"},}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.BoolFlag{Name: "gateway-ports", Aliases: []string{"g"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "identity-file", Aliases: []string{"i"},}, - &console.StringFlag{Name: "port",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "relationship", Aliases: []string{"r"},}, + Usage: "Open a single SSH tunnel to an app relationship", + Flags: []console.Flag{ + &console.StringFlag{Name: "app", Aliases: []string{"A"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "gateway-ports", Aliases: []string{"g"}}, + &console.StringFlag{Name: "port"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "relationship", Aliases: []string{"r"}}, }, }, { Category: "cloud:user", Name: "add", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "user:add", Hidden: true}, + {Name: "upsun:user:add", Hidden: true}, }, - Usage: "Add a user to the project", - Flags: []console.Flag{ - &console.BoolFlag{Name: "force-invite",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "role", Aliases: []string{"r"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Add a user to the project", + Flags: []console.Flag{ + &console.BoolFlag{Name: "force-invite"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "role", Aliases: []string{"r"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:user", Name: "delete", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "user:delete", Hidden: true}, + {Name: "upsun:user:delete", Hidden: true}, }, - Usage: "Delete a user from the project", - Flags: []console.Flag{ - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Delete a user from the project", + Flags: []console.Flag{ + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:user", Name: "get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "user:get", Hidden: true}, - {Name: "cloud:user:role"}, - {Name: "user:role", Hidden: true}, + {Name: "upsun:user:get", Hidden: true}, }, - Usage: "View a user's role(s)", - Flags: []console.Flag{ - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "level", Aliases: []string{"l"},}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.BoolFlag{Name: "pipe",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "role", Aliases: []string{"r"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "View a user's role(s)", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "level", Aliases: []string{"l"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.BoolFlag{Name: "pipe"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "role", Aliases: []string{"r"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:user", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "user:list", Hidden: true}, + {Name: "upsun:user:list", Hidden: true}, {Name: "cloud:users"}, + {Name: "upsun:users", Hidden: true}, {Name: "users", Hidden: true}, }, - Usage: "List project users", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "List project users", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:user", Name: "update", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "user:update", Hidden: true}, + {Name: "upsun:user:update", Hidden: true}, }, - Usage: "Update user role(s) on a project", - Flags: []console.Flag{ - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "role", Aliases: []string{"r"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Update user role(s) on a project", + Flags: []console.Flag{ + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "role", Aliases: []string{"r"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:variable", Name: "create", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "variable:create", Hidden: true}, - }, - Usage: "Create a variable", - Flags: []console.Flag{ - &console.BoolFlag{Name: "enabled", DefaultValue: true,}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "inheritable", DefaultValue: true,}, - &console.BoolFlag{Name: "json",}, - &console.StringFlag{Name: "level", Aliases: []string{"l"},}, - &console.StringFlag{Name: "name",}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "prefix", DefaultValue: "none",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "sensitive",}, - &console.StringFlag{Name: "value",}, - &console.StringFlag{Name: "visible-build",}, - &console.BoolFlag{Name: "visible-runtime", DefaultValue: true,}, - &console.BoolFlag{Name: "wait",}, + {Name: "upsun:variable:create", Hidden: true}, + }, + Usage: "Create a variable", + Flags: []console.Flag{ + &console.BoolFlag{Name: "enabled", DefaultValue: true}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "inheritable", DefaultValue: true}, + &console.BoolFlag{Name: "json"}, + &console.StringFlag{Name: "level", Aliases: []string{"l"}}, + &console.StringFlag{Name: "name"}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "prefix", DefaultValue: "none"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "sensitive"}, + &console.BoolFlag{Name: "update", Aliases: []string{"u"}}, + &console.StringFlag{Name: "value"}, + &console.StringFlag{Name: "visible-build"}, + &console.BoolFlag{Name: "visible-runtime", DefaultValue: true}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:variable", Name: "delete", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "variable:delete", Hidden: true}, + {Name: "upsun:variable:delete", Hidden: true}, }, - Usage: "Delete a variable", - Flags: []console.Flag{ - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "level", Aliases: []string{"l"},}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "wait",}, + Usage: "Delete a variable", + Flags: []console.Flag{ + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "level", Aliases: []string{"l"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "wait"}, }, }, { Category: "cloud:variable", Name: "get", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "variable:get", Hidden: true}, + {Name: "upsun:variable:get", Hidden: true}, {Name: "cloud:vget"}, + {Name: "upsun:vget", Hidden: true}, {Name: "vget", Hidden: true}, }, - Usage: "View a variable", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "level", Aliases: []string{"l"},}, - &console.BoolFlag{Name: "no-header",}, - &console.BoolFlag{Name: "pipe",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.StringFlag{Name: "property", Aliases: []string{"P"},}, + Usage: "View a variable", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.StringFlag{Name: "level", Aliases: []string{"l"}}, + &console.BoolFlag{Name: "no-header"}, + &console.BoolFlag{Name: "pipe"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.StringFlag{Name: "property", Aliases: []string{"P"}}, }, }, { Category: "cloud:variable", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "variable:list", Hidden: true}, + {Name: "upsun:variable:list", Hidden: true}, {Name: "cloud:variables"}, + {Name: "upsun:variables", Hidden: true}, {Name: "variables", Hidden: true}, {Name: "cloud:var"}, + {Name: "upsun:var", Hidden: true}, {Name: "var", Hidden: true}, }, - Usage: "List variables", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.StringFlag{Name: "level", Aliases: []string{"l"},}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, + Usage: "List variables", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.StringFlag{Name: "level", Aliases: []string{"l"}}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:variable", Name: "update", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "variable:update", Hidden: true}, + {Name: "upsun:variable:update", Hidden: true}, + }, + Usage: "Update a variable", + Flags: []console.Flag{ + &console.BoolFlag{Name: "allow-no-change"}, + &console.BoolFlag{Name: "enabled", DefaultValue: true}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.BoolFlag{Name: "inheritable", DefaultValue: true}, + &console.BoolFlag{Name: "json"}, + &console.StringFlag{Name: "level", Aliases: []string{"l"}}, + &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"}}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "sensitive"}, + &console.StringFlag{Name: "value"}, + &console.StringFlag{Name: "visible-build"}, + &console.BoolFlag{Name: "visible-runtime", DefaultValue: true}, + &console.BoolFlag{Name: "wait"}, + }, + }, + { + Category: "cloud:version", + Name: "list", + Aliases: []*console.Alias{ + {Name: "version:list", Hidden: true}, + {Name: "upsun:version:list", Hidden: true}, + {Name: "cloud:versions"}, + {Name: "upsun:versions", Hidden: true}, + {Name: "versions", Hidden: true}, }, - Usage: "Update a variable", - Flags: []console.Flag{ - &console.BoolFlag{Name: "enabled", DefaultValue: true,}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "inheritable", DefaultValue: true,}, - &console.BoolFlag{Name: "json",}, - &console.StringFlag{Name: "level", Aliases: []string{"l"},}, - &console.BoolFlag{Name: "no-wait", Aliases: []string{"W"},}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "sensitive",}, - &console.StringFlag{Name: "value",}, - &console.StringFlag{Name: "visible-build",}, - &console.BoolFlag{Name: "visible-runtime", DefaultValue: true,}, - &console.BoolFlag{Name: "wait",}, + Usage: "ALPHA List environment versions", + Hidden: console.Hide, + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, }, }, { Category: "cloud:worker", Name: "list", - Aliases: []*console.Alias{ + Aliases: []*console.Alias{ {Name: "worker:list", Hidden: true}, + {Name: "upsun:worker:list", Hidden: true}, {Name: "cloud:workers"}, + {Name: "upsun:workers", Hidden: true}, {Name: "workers", Hidden: true}, }, - Usage: "Get a list of all deployed workers", - Flags: []console.Flag{ - &console.StringFlag{Name: "columns",}, - &console.StringFlag{Name: "environment", Aliases: []string{"e"},}, - &console.StringFlag{Name: "format", DefaultValue: "table",}, - &console.StringFlag{Name: "host",}, - &console.BoolFlag{Name: "no-header",}, - &console.StringFlag{Name: "project", Aliases: []string{"p"},}, - &console.BoolFlag{Name: "refresh",}, + Usage: "Get a list of all deployed workers", + Flags: []console.Flag{ + &console.StringFlag{Name: "columns", Aliases: []string{"c"}}, + &console.StringFlag{Name: "environment", Aliases: []string{"e"}}, + &console.StringFlag{Name: "format", DefaultValue: "table"}, + &console.BoolFlag{Name: "no-header"}, + &console.BoolFlag{Name: "pipe"}, + &console.StringFlag{Name: "project", Aliases: []string{"p"}}, + &console.BoolFlag{Name: "refresh"}, }, }, } diff --git a/local/platformsh/config.go b/local/platformsh/config.go index b9309f0b..40e42bb0 100644 --- a/local/platformsh/config.go +++ b/local/platformsh/config.go @@ -23,108 +23,118 @@ package platformsh var availablePHPExts = map[string][]string{ - "amqp": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "apc": {"5.4", "5.5"}, - "apcu": {"5.4", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, + "amqp": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "apc": {"5.4"}, + "apcu": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, "apcu_bc": {"7.0", "7.1", "7.2", "7.3", "7.4"}, - "applepay": {"7.0", "7.1", "7.4"}, - "bcmath": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "blackfire": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "bz2": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "calendar": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "ctype": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "curl": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "dba": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "dom": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "enchant": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "event": {"7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "exif": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "ffi": {"7.4", "8.0", "8.1"}, - "fileinfo": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "ftp": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "gd": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, + "applepay": {"7.0", "7.1", "7.3", "7.4"}, + "bcmath": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "blackfire": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "bz2": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "calendar": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "ctype": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "curl": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "datadog": {"8.2", "8.3"}, + "dba": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "dom": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "enchant": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "event": {"7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "exif": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "ffi": {"7.4", "8.0", "8.1", "8.2", "8.3"}, + "fileinfo": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "ftp": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "gd": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, "gearman": {"5.4", "5.5", "5.6"}, - "geoip": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4"}, - "gettext": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "gmp": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "http": {"5.4", "5.5", "7.3", "7.4", "8.0", "8.1"}, - "iconv": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "igbinary": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "imagick": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "imap": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "interbase": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0"}, - "intl": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "ioncube": {"7.0", "7.1", "7.2"}, - "json": {"5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "ldap": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "mailparse": {"7.0", "7.1", "7.2", "7.4", "8.0", "8.1"}, - "mbstring": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, + "geoip": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "gettext": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "gmp": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "gnupg": {"8.2", "8.3"}, + "http": {"5.4", "5.5", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "iconv": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "igbinary": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "imagick": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0"}, + "imap": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "interbase": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2"}, + "intl": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "ioncube": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2"}, + "json": {"5.6", "7.0", "7.1", "7.2", "7.3", "7.4"}, + "ldap": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "mailparse": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "mbstring": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, "mcrypt": {"5.4", "5.5", "5.6", "7.0", "7.1"}, "memcache": {"5.4", "5.5", "5.6"}, - "memcached": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0"}, + "memcached": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, "mongo": {"5.4", "5.5", "5.6"}, - "mongodb": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "msgpack": {"5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0"}, + "mongodb": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "msgpack": {"5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, "mssql": {"5.4", "5.5", "5.6"}, - "mysql": {"5.4", "5.5", "5.6"}, - "mysqli": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "mysqlnd": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "newrelic": {"5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "oauth": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "odbc": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "opcache": {"5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "pdo": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "pdo_dblib": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "pdo_firebird": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4"}, - "pdo_mysql": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "pdo_odbc": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "pdo_pgsql": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "pdo_sqlite": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "pdo_sqlsrv": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "pgsql": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "phar": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, + "mysql": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "mysqli": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "mysqlnd": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "newrelic": {"5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "oauth": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "odbc": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "opcache": {"5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "openswoole": {"8.2", "8.3"}, + "opentelemetry": {"8.2", "8.3"}, + "pdo": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "pdo_dblib": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "pdo_firebird": {"5.4", "5.5", "5.6", "7.0", "7.1", "8.2", "8.3", "8.4"}, + "pdo_mysql": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "pdo_odbc": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "pdo_pgsql": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "pdo_sqlite": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "pdo_sqlsrv": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "pecl-http": {"5.6"}, + "pgsql": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "phar": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "phpdbg": {"5.6"}, "pinba": {"5.4", "5.5", "5.6"}, - "posix": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, + "posix": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, "propro": {"5.6"}, - "pspell": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "pthreads": {"7.1"}, - "raphf": {"5.6", "7.4", "8.0", "8.1"}, - "readline": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, + "protobuf": {"8.1"}, + "pspell": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "pthreads": {"7.1", "7.2"}, + "raphf": {"5.6", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "rdkafka": {"8.1", "8.2", "8.3"}, + "readline": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, "recode": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3"}, - "redis": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "shmop": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "simplexml": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "snmp": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "soap": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "sockets": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "sodium": {"7.2", "7.3", "7.4", "8.0", "8.1"}, - "sourceguardian": {"7.0", "7.1"}, + "redis": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "shmop": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "simplexml": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "snmp": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "soap": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "sockets": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "sodium": {"7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "sourceguardian": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, "spplus": {"5.4", "5.5"}, - "sqlite3": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "sqlsrv": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "ssh2": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "sybase": {"7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "sysvmsg": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "sysvsem": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "sysvshm": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "tideways": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, + "sqlite3": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "sqlsrv": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "ssh2": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "swoole": {"8.2", "8.3"}, + "sybase": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "sysvmsg": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "sysvsem": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "sysvshm": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "tideways": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2"}, "tideways_xhprof": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "tidy": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "tokenizer": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "uuid": {"7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, + "tidy": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "tokenizer": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "uuid": {"7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, + "uv": {"8.3"}, "wddx": {"7.0", "7.1", "7.2", "7.3", "7.4"}, - "xdebug": {"7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, "xcache": {"5.4", "5.5"}, + "xdebug": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, "xhprof": {"5.4", "5.5", "5.6"}, - "xml": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "xmlreader": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "xmlrpc": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.1"}, - "xmlwriter": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "xsl": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "yaml": {"7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, + "xml": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "xmlreader": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "xmlrpc": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "xmlwriter": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "xsl": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, + "yaml": {"7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"}, "zbarcode": {"7.0", "7.1", "7.2", "7.3"}, - "zendopcache": {"5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, - "zip": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"}, + "zendopcache": {"5.4"}, + "zip": {"7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"}, } var availableServices = []*service{ @@ -132,35 +142,35 @@ var availableServices = []*service{ Type: "chrome-headless", Versions: serviceVersions{ Deprecated: []string{}, - Supported: []string{"73", "80", "81", "83", "84", "86", "91"}, + Supported: []string{"73", "80", "81", "83", "84", "86", "91", "95", "113", "120"}, }, }, { Type: "elasticsearch", Versions: serviceVersions{ - Deprecated: []string{"0.9", "1.4", "1.7", "2.4", "5.2", "5.4", "6.5", "6.8", "7.2", "7.5", "7.7", "7.9", "7.10"}, - Supported: []string{}, + Deprecated: []string{"1.4", "1.7", "2.4", "5.2", "5.4", "6.5", "6.8", "7.2", "7.5", "7.7", "7.9", "7.10"}, + Supported: []string{"7.17", "8.5"}, }, }, { Type: "influxdb", Versions: serviceVersions{ - Deprecated: []string{}, - Supported: []string{"1.2", "1.3", "1.7", "1.8"}, + Deprecated: []string{"1.2", "1.3", "1.7", "1.8", "2.2"}, + Supported: []string{"2.3", "2.7"}, }, }, { Type: "kafka", Versions: serviceVersions{ - Deprecated: []string{}, - Supported: []string{"2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7"}, + Deprecated: []string{"2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7"}, + Supported: []string{"3.2", "3.4", "3.6", "3.7"}, }, }, { Type: "mariadb", Versions: serviceVersions{ - Deprecated: []string{"5.5"}, - Supported: []string{"10.0", "10.1", "10.2", "10.3", "10.4", "10.5"}, + Deprecated: []string{"5.5", "10.0", "10.1", "10.2", "10.3"}, + Supported: []string{"10.4", "10.5", "10.6", "10.11", "11.0", "11.2", "11.4"}, }, }, { @@ -173,22 +183,22 @@ var availableServices = []*service{ { Type: "mongodb", Versions: serviceVersions{ - Deprecated: []string{"3.0", "3.2", "3.4", "3.6"}, - Supported: []string{}, + Deprecated: []string{"3.0", "3.2", "3.4", "3.6", "4.0.3"}, + Supported: []string{}, }, }, { Type: "mongodb-enterprise", Versions: serviceVersions{ - Deprecated: []string{}, - Supported: []string{"5.0"}, + Deprecated: []string{"4.0", "4.2"}, + Supported: []string{"4.4", "5.0", "6.0", "7.0"}, }, }, { Type: "mysql", Versions: serviceVersions{ - Deprecated: []string{"5.5"}, - Supported: []string{"10.0", "10.1", "10.2", "10.3", "10.4", "10.5"}, + Deprecated: []string{"5.5", "10.0", "10.1", "10.2"}, + Supported: []string{"10.3", "10.4", "10.5", "10.6", "10.11", "11.0"}, }, }, { @@ -201,8 +211,8 @@ var availableServices = []*service{ { Type: "opensearch", Versions: serviceVersions{ - Deprecated: []string{}, - Supported: []string{"1.1", "1.2"}, + Deprecated: []string{"1.1", "1.2"}, + Supported: []string{"1", "2"}, }, }, { @@ -215,43 +225,43 @@ var availableServices = []*service{ { Type: "postgresql", Versions: serviceVersions{ - Deprecated: []string{"9.3", "9.4", "9.5"}, - Supported: []string{"9.6", "10", "11", "12", "13"}, + Deprecated: []string{"9.3", "9.4", "9.5", "9.6", "10", "11"}, + Supported: []string{"12", "13", "14", "15", "16", "17"}, }, }, { Type: "rabbitmq", Versions: serviceVersions{ - Deprecated: []string{}, - Supported: []string{"3.5", "3.6", "3.7", "3.8", "3.9"}, + Deprecated: []string{"3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"}, + Supported: []string{"3.12", "3.13", "4.0", "4.1"}, }, }, { Type: "redis", Versions: serviceVersions{ - Deprecated: []string{"2.8", "3.0"}, - Supported: []string{"3.2", "4.0", "5.0", "6.0", "6.2", "7.0"}, + Deprecated: []string{"2.8", "3.0", "3.2", "4.0", "5.0", "6.0"}, + Supported: []string{"6.2", "7.0", "7.2"}, }, }, { Type: "solr", Versions: serviceVersions{ - Deprecated: []string{"3.6", "4.10", "6.3", "6.6", "7.6"}, - Supported: []string{"7.7", "8.0", "8.4", "8.6"}, + Deprecated: []string{"3.6", "4.10", "6.3", "6.6", "7.6", "7.7", "8.0", "8.4", "8.6"}, + Supported: []string{"8.11", "9.1", "9.2", "9.4", "9.6"}, }, }, { Type: "varnish", Versions: serviceVersions{ - Deprecated: []string{}, - Supported: []string{"5.1", "5.2", "6.0", "6.3"}, + Deprecated: []string{"5.1", "5.2", "6.3", "6.4", "7.1"}, + Supported: []string{"6.0", "7.2", "7.3", "7.6"}, }, }, { Type: "vault-kms", Versions: serviceVersions{ - Deprecated: []string{}, - Supported: []string{"1.6", "1.8"}, + Deprecated: []string{"1.6", "1.8"}, + Supported: []string{"1.12"}, }, }, } diff --git a/local/platformsh/db_versions.go b/local/platformsh/db_versions.go new file mode 100644 index 00000000..6086d072 --- /dev/null +++ b/local/platformsh/db_versions.go @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package platformsh + +import ( + "errors" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/joho/godotenv" + "github.com/rs/zerolog" + "gopkg.in/yaml.v2" +) + +type serviceConfigs map[string]struct { + Type string `yaml:"type"` +} + +func ReadDBVersionFromPlatformServiceYAML(projectDir string, logger zerolog.Logger) (string, string, string) { + // Platform.sh + configFile := filepath.Join(".platform", "services.yaml") + if servicesYAML, err := os.ReadFile(filepath.Join(projectDir, configFile)); err == nil { + var services serviceConfigs + if err := yaml.Unmarshal(servicesYAML, &services); err == nil { + if dbName, dbVersion, err := extractCloudDatabaseType(services); err == nil { + logger.Debug().Msg("DB configured in .platform/services.yaml") + return configFile, dbName, dbVersion + } else { + logger.Debug().Msg("No DB configured in .platform/services.yaml") + } + } else { + logger.Debug().Msg("Unable to parse .platform/services.yaml file") + } + } else { + logger.Debug().Msg("No .platform/services.yaml file found or not readable") + } + + // Upsun + upsunDir := filepath.Join(projectDir, ".upsun") + if _, err := os.Stat(upsunDir); err == nil { + if files, err := os.ReadDir(upsunDir); err == nil { + for _, file := range files { + configFile := filepath.Join(".upsun", file.Name()) + if servicesYAML, err := os.ReadFile(filepath.Join(projectDir, configFile)); err == nil { + var config struct { + Services serviceConfigs `yaml:"services"` + } + if err := yaml.Unmarshal(servicesYAML, &config); err == nil { + if dbName, dbVersion, err := extractCloudDatabaseType(config.Services); err == nil { + logger.Debug().Msgf("DB configured in %s", configFile) + return configFile, dbName, dbVersion + } else { + logger.Debug().Msgf("No DB configured in %s", configFile) + } + } else { + logger.Debug().Msgf("Unable to parse the %s file", configFile) + } + } else { + logger.Debug().Msgf("Unable to read the %s file", configFile) + } + } + } else { + logger.Debug().Msg("Unable to list files under the .upsun directory") + } + } else { + logger.Debug().Msg("No .upsun directory found") + } + logger.Debug().Msg("No DB configured") + return "", "", "" +} + +func extractCloudDatabaseType(services serviceConfigs) (string, string, error) { + dbName := "" + dbVersion := "" + for _, service := range services { + if strings.HasPrefix(service.Type, "mysql") || strings.HasPrefix(service.Type, "mariadb") || strings.HasPrefix(service.Type, "postgresql") { + if dbName != "" { + // give up as there are multiple DBs + return "", "", nil + } + + parts := strings.Split(service.Type, ":") + dbName = parts[0] + dbVersion = parts[1] + } + } + return dbName, dbVersion, nil +} + +func ReadDBVersionFromDotEnv(projectDir string) (string, error) { + path := filepath.Join(projectDir, ".env") + if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { + return "", nil + } + + vars, err := godotenv.Read(path) + if err != nil { + return "", err + } + + databaseURL, defined := vars["DATABASE_URL"] + if !defined { + return "", nil + } + + if !strings.Contains(databaseURL, "serverVersion=") { + return "", nil + } + + url, err := url.Parse(databaseURL) + if err != nil { + return "", err + } + + return url.Query().Get("serverVersion"), nil +} + +func ReadDBVersionFromDoctrineConfigYAML(projectDir string) (string, error) { + path := filepath.Join(projectDir, "config", "packages", "doctrine.yaml") + if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { + return "", nil + } + + doctrineConfigYAML, err := os.ReadFile(path) + if err != nil { + return "", err + } + + var doctrineConfig struct { + Doctrine struct { + Dbal struct { + ServerVersion string `yaml:"server_version"` + Connections struct { + Default struct { + ServerVersion string `yaml:"server_version"` + } `yaml:"default"` + } + } `yaml:"dbal"` + } `yaml:"doctrine"` + } + if err := yaml.Unmarshal(doctrineConfigYAML, &doctrineConfig); err != nil { + // format is wrong + return "", err + } + + version := doctrineConfig.Doctrine.Dbal.Connections.Default.ServerVersion + if version == "" { + version = doctrineConfig.Doctrine.Dbal.ServerVersion + } + if version == "" { + // empty version + return "", nil + } + if version[0] == '%' && version[len(version)-1] == '%' { + // references an env var, ignore + return "", nil + } + return version, nil +} + +func DatabaseVersiondUnsynced(providedVersion, dbVersion string) bool { + providedVersion = strings.Replace(providedVersion, "mariadb-", "", 1) + + return providedVersion != "" && !strings.HasPrefix(providedVersion, dbVersion) +} diff --git a/local/platformsh/db_versions_test.go b/local/platformsh/db_versions_test.go new file mode 100644 index 00000000..a515c31c --- /dev/null +++ b/local/platformsh/db_versions_test.go @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package platformsh + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type PlatformSuite struct{} + +var _ = Suite(&PlatformSuite{}) + +func (s *PlatformSuite) TestReadDBVersionFromDoctrineConfigYAML(c *C) { + version, err := ReadDBVersionFromDoctrineConfigYAML("testdata/projectA") + c.Assert(err, IsNil) + c.Assert(version, Equals, "") +} diff --git a/local/platformsh/generator/commands.go b/local/platformsh/generator/commands.go index c0874060..b99a16a0 100644 --- a/local/platformsh/generator/commands.go +++ b/local/platformsh/generator/commands.go @@ -1,54 +1,41 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + package main import ( "bytes" - "crypto/md5" "encoding/json" "fmt" - "io/ioutil" + "go/format" "os" - "path/filepath" + "os/exec" "sort" "strings" "text/template" "github.com/mitchellh/go-homedir" - "github.com/pkg/errors" - "github.com/symfony-cli/symfony-cli/local/php" + "github.com/symfony-cli/console" + "github.com/symfony-cli/symfony-cli/local/platformsh" + "github.com/symfony-cli/symfony-cli/symfony" ) -type application struct { - Namespaces []namespace - Commands []command -} - -type namespace struct { - ID string - Commands []string -} - -type command struct { - Name string - Usage []string - Description string - Help string - Definition definition - Hidden bool -} - -type definition struct { - Arguments map[string]argument - Options map[string]option -} - -type argument struct { -} - -type option struct { - Shortcut string - Default interface{} -} - var commandsTemplate = template.Must(template.New("output").Parse(`// Code generated by platformsh/generator/main.go // DO NOT EDIT @@ -87,10 +74,12 @@ func generateCommands() { if err != nil { panic(err) } - if err := php.InstallPlatformPhar(home); err != nil { + // as platform.sh and upsun have the same commands, we can use either one + cloudPath, err := platformsh.Install(home, platformsh.PlatformshBrand) + if err != nil { panic(err.Error()) } - definitionAsString, err := parseCommands(home) + definitionAsString, err := parseCommands(cloudPath) if err != nil { panic(err.Error()) } @@ -105,44 +94,22 @@ func generateCommands() { if err != nil { panic(err) } - f.Write(buf.Bytes()) - -} - -func parseCommands(home string) (string, error) { - dir := filepath.Join(home, ".platformsh", "bin") - var pharPath = filepath.Join(dir, "platform") - hasher := md5.New() - if s, err := ioutil.ReadFile(pharPath); err != nil { - hasher.Write(s) - } - - var buf bytes.Buffer - e := &php.Executor{ - BinName: "php", - Args: []string{"php", filepath.Join(dir, "platform"), "list", "--format=json"}, - } - e.Paths = append([]string{dir}, e.Paths...) - e.Dir = dir - e.Stdout = &buf - if ret := e.Execute(false); ret != 0 { - return "", errors.Errorf("unable to list commands: %s", buf.String()) + source, err := format.Source(buf.Bytes()) + if err != nil { + panic(err) } + f.Write(source) - // Fix PHP types - cleanOutput := bytes.ReplaceAll(buf.Bytes(), []byte(`"arguments":[]`), []byte(`"arguments":{}`)) +} - var definition application - if err := json.Unmarshal(cleanOutput, &definition); err != nil { +func parseCommands(cloudPath string) (string, error) { + wd, err := os.Getwd() + if err != nil { return "", err } - - allCommandNames := map[string]bool{} - for _, n := range definition.Namespaces { - for _, name := range n.Commands { - allCommandNames[name] = true - } - // FIXME: missing the aliases here + cliApp, err := symfony.NewGoCliApp(wd, cloudPath, []string{"--all"}) + if err != nil { + return "", err } excludedCommands := map[string]bool{ @@ -151,9 +118,19 @@ func parseCommands(home string) (string, error) { "self:stats": true, "decode": true, "environment:drush": true, + "project:init": true, } + + excludedOptions := console.AnsiFlag.Names() + excludedOptions = append(excludedOptions, console.NoAnsiFlag.Names()...) + excludedOptions = append(excludedOptions, console.NoInteractionFlag.Names()...) + excludedOptions = append(excludedOptions, console.QuietFlag.Names()...) + excludedOptions = append(excludedOptions, console.LogLevelFlag.Names()...) + excludedOptions = append(excludedOptions, console.HelpFlag.Names()...) + excludedOptions = append(excludedOptions, console.VersionFlag.Names()...) + definitionAsString := "" - for _, command := range definition.Commands { + for _, command := range cliApp.Commands { if strings.Contains(command.Description, "deprecated") || strings.Contains(command.Description, "DEPRECATED") { continue } @@ -168,7 +145,7 @@ func parseCommands(home string) (string, error) { } namespace := "cloud" loop: - for _, n := range definition.Namespaces { + for _, n := range cliApp.Namespaces { for _, name := range n.Commands { if name == command.Name { if n.ID != "_global" { @@ -183,21 +160,27 @@ func parseCommands(home string) (string, error) { if namespace != "cloud" && !strings.HasPrefix(command.Name, "self:") { aliases = append(aliases, fmt.Sprintf("{Name: \"%s\", Hidden: true}", command.Name)) } - for _, usage := range command.Usage { - if allCommandNames[usage] { - aliases = append(aliases, fmt.Sprintf("{Name: \"cloud:%s\"}", usage)) - if namespace != "cloud" && !strings.HasPrefix(command.Name, "self:") { - aliases = append(aliases, fmt.Sprintf("{Name: \"%s\", Hidden: true}", usage)) - } + + cmdAliases, err := getCommandAliases(command.Name, cloudPath) + if err != nil { + return "", err + } + aliases = append(aliases, fmt.Sprintf("{Name: \"upsun:%s\", Hidden: true}", command.Name)) + for _, alias := range cmdAliases { + aliases = append(aliases, fmt.Sprintf("{Name: \"cloud:%s\"}", alias)) + aliases = append(aliases, fmt.Sprintf("{Name: \"upsun:%s\", Hidden: true}", alias)) + if namespace != "cloud" && !strings.HasPrefix(command.Name, "self:") { + aliases = append(aliases, fmt.Sprintf("{Name: \"%s\", Hidden: true}", alias)) } } if command.Name == "environment:push" { aliases = append(aliases, "{Name: \"deploy\"}") aliases = append(aliases, "{Name: \"cloud:deploy\"}") + aliases = append(aliases, "{Name: \"upsun:deploy\", Hidden: true}") } aliasesAsString := "" if len(aliases) > 0 { - aliasesAsString += "\n\t\tAliases: []*console.Alias{\n" + aliasesAsString += "\n\t\tAliases: []*console.Alias{\n" for _, alias := range aliases { aliasesAsString += "\t\t\t" + alias + ",\n" } @@ -205,19 +188,27 @@ func parseCommands(home string) (string, error) { } hide := "" if command.Hidden { - hide = "\n\t\tHidden: console.Hide," + hide = "\n\t\tHidden: console.Hide," } optionNames := make([]string, 0, len(command.Definition.Options)) + + optionsLoop: for name := range command.Definition.Options { + if name == "yes" || name == "no" || name == "version" { + continue + } + for _, excludedOption := range excludedOptions { + if excludedOption == name { + continue optionsLoop + } + } + optionNames = append(optionNames, name) } sort.Strings(optionNames) flags := []string{} for _, name := range optionNames { - if name == "yes" || name == "no" || name == "help" || name == "quiet" || name == "verbose" || name == "version" { - continue - } option := command.Definition.Options[name] optionAliasesAsString := "" if option.Shortcut != "" { @@ -243,20 +234,40 @@ func parseCommands(home string) (string, error) { } flagsAsString := "" if len(flags) > 0 { - flagsAsString += "\n\t\tFlags: []console.Flag{\n" + flagsAsString += "\n\t\tFlags: []console.Flag{\n" for _, flag := range flags { flagsAsString += "\t\t\t" + flag + ",\n" } flagsAsString += "\t\t}," } + command.Description = strings.ReplaceAll(command.Description, "Platform.sh", "Platform.sh/Upsun") definitionAsString += fmt.Sprintf(` { Category: "%s", - Name: "%s",%s - Usage: %#v,%s%s + Name: "%s",%s + Usage: %#v,%s%s }, `, namespace, name, aliasesAsString, command.Description, hide, flagsAsString) } return definitionAsString, nil } + +func getCommandAliases(name, cloudPath string) ([]string, error) { + var buf bytes.Buffer + var bufErr bytes.Buffer + c := exec.Command(cloudPath, name, "--help", "--format=json") + c.Stdout = &buf + c.Stderr = &bufErr + if err := c.Run(); err != nil { + // Can currently happen for commands implemented in Go upstream (like app:config-validate) + // FIXME: to be removed once upstream implements --help --format=json for all commands + return []string{}, nil + //return nil, errors.Errorf("unable to get definition for command %s: %s\n%s\n%s", name, err, bufErr.String(), buf.String()) + } + var cmd symfony.CliCommand + if err := json.Unmarshal(buf.Bytes(), &cmd); err != nil { + return nil, err + } + return cmd.Aliases, nil +} diff --git a/local/platformsh/generator/config.go b/local/platformsh/generator/config.go index fe7a18f9..851eb8da 100644 --- a/local/platformsh/generator/config.go +++ b/local/platformsh/generator/config.go @@ -1,17 +1,38 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + package main import ( - "bufio" "bytes" "encoding/json" "fmt" - "io/ioutil" - "log" + "go/format" + "io" "net/http" "os" "sort" "strings" "text/template" + + "github.com/hashicorp/go-version" + "gopkg.in/yaml.v2" ) type service struct { @@ -81,17 +102,21 @@ func generateConfig() { if err != nil { panic(err) } - f.Write(buf.Bytes()) + source, err := format.Source(buf.Bytes()) + if err != nil { + panic(err) + } + f.Write(source) } func parseServices() (string, error) { - resp, err := http.Get("https://raw.githubusercontent.com/platformsh/platformsh-docs/master/docs/data/registry.json") + resp, err := http.Get("https://raw.githubusercontent.com/platformsh/platformsh-docs/master/shared/data/registry.json") if err != nil { return "", err } defer resp.Body.Close() var services map[string]*service - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return "", err } @@ -107,16 +132,25 @@ func parseServices() (string, error) { for _, name := range serviceNames { s := services[name] if !s.Runtime { + deprecatedVersions, err := sortVersions(s.Versions.Deprecated) + if err != nil { + return "", err + } + supportedVersions, err := sortVersions(s.Versions.Supported) + if err != nil { + return "", err + } + servicesAsString += "\t{\n" servicesAsString += fmt.Sprintf("\t\tType: \"%s\",\n", s.Type) servicesAsString += "\t\tVersions: serviceVersions{\n" - if len(s.Versions.Deprecated) > 0 { - servicesAsString += fmt.Sprintf("\t\t\tDeprecated: []string{\"%s\"},\n", strings.Join(s.Versions.Deprecated, "\", \"")) + if len(deprecatedVersions) > 0 { + servicesAsString += fmt.Sprintf("\t\t\tDeprecated: []string{\"%s\"},\n", strings.Join(deprecatedVersions, "\", \"")) } else { servicesAsString += "\t\t\tDeprecated: []string{},\n" } - if len(s.Versions.Supported) > 0 { - servicesAsString += fmt.Sprintf("\t\t\tSupported: []string{\"%s\"},\n", strings.Join(s.Versions.Supported, "\", \"")) + if len(supportedVersions) > 0 { + servicesAsString += fmt.Sprintf("\t\t\tSupported: []string{\"%s\"},\n", strings.Join(supportedVersions, "\", \"")) } else { servicesAsString += "\t\t\tSupported: []string{},\n" } @@ -128,7 +162,7 @@ func parseServices() (string, error) { } func parsePHPExtensions() (string, error) { - resp, err := http.Get("https://raw.githubusercontent.com/platformsh/platformsh-docs/master/docs/src/languages/php/extensions.md") + resp, err := http.Get("https://raw.githubusercontent.com/platformsh/platformsh-docs/master/shared/data/php_extensions.yaml") if err != nil { return "", err } @@ -136,45 +170,37 @@ func parsePHPExtensions() (string, error) { var versions []string orderedExtensionNames := []string{} extensions := make(map[string][]string) - started := false - scanner := bufio.NewScanner(resp.Body) - for scanner.Scan() { - line := scanner.Text() - if started { - if strings.HasPrefix(line, "| ---") { - continue - } - if !strings.HasPrefix(line, "| ") { - break - } - name, available := parseLine(line) - name = strings.ToLower(strings.Trim(name, "`")) - if _, ok := extensions[name]; ok { - log.Printf("WARNING: The %s extension is listed twice, ignoring extra definition!\n", name) - } else { - orderedExtensionNames = append(orderedExtensionNames, name) - var vs []string - for i, v := range available { - if v != "" { - vs = append(vs, versions[i]) - } - } - extensions[name] = vs - } - } - if strings.HasPrefix(line, "| Extension") { - started = true - _, versions = parseLine(line) + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + var fullConfig struct { + Grid map[string]struct { + Available []string + Default []string } } - if err := scanner.Err(); err != nil { + if err := yaml.Unmarshal(body, &fullConfig); err != nil { return "", err } + for version, cfg := range fullConfig.Grid { + for _, ext := range append(cfg.Available, cfg.Default...) { + name := strings.ToLower(ext) + if _, ok := extensions[name]; !ok { + orderedExtensionNames = append(orderedExtensionNames, name) + } + extensions[name] = append(extensions[name], version) + } + } + + sort.Strings(orderedExtensionNames) maxNameLen := 0 for name := range extensions { if len(name) > maxNameLen { maxNameLen = len(name) } + sort.Strings(extensions[name]) } extsAsString := "" @@ -193,21 +219,19 @@ func parsePHPExtensions() (string, error) { return extsAsString, nil } -func parseLine(line string) (string, []string) { - next := strings.Index(line[1:], "|") + 1 - name := strings.TrimSpace(line[1:next]) - var versions []string - for { - current := next + 1 - nextIndex := strings.Index(line[current:], "|") - if nextIndex == -1 { - break - } - next = nextIndex + current - versions = append(versions, strings.TrimSpace(line[current:next])) - if next >= len(line) { - break +func sortVersions(versions []string) ([]string, error) { + parsedVersions := make([]*version.Version, len(versions)) + for i, raw := range versions { + v, err := version.NewVersion(raw) + if err != nil { + return nil, err } + parsedVersions[i] = v + } + sort.Sort(version.Collection(parsedVersions)) + versionsAsStrings := make([]string, len(versions)) + for i, version := range parsedVersions { + versionsAsStrings[i] = version.Original() } - return name, versions + return versionsAsStrings, nil } diff --git a/local/platformsh/installer.go b/local/platformsh/installer.go new file mode 100644 index 00000000..d5589a77 --- /dev/null +++ b/local/platformsh/installer.go @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2023-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package platformsh + +import ( + "archive/tar" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/blackfireio/osinfo" + "github.com/pkg/errors" + "github.com/schollz/progressbar/v3" + "github.com/symfony-cli/terminal" +) + +type githubAsset struct { + Name string + URL string `json:"browser_download_url"` + version string +} + +type versionCheck struct { + CurrentVersion string + Timestamp int64 +} + +// Install installs or updates the Platform.sh CLI tool. +func Install(home string, brand CloudBrand) (string, error) { + binPath := filepath.Join(home, brand.BinaryPath()) + versionCheckPath := binPath + ".json" + + // do we already have the binary? + binExists := false + if _, err := os.Stat(binPath); err == nil { + binExists = true + versionCheck := loadVersionCheck(versionCheckPath) + if versionCheck == nil { + // we need to download the bin again as we don't have the version info anymore, so it will never be updated! + goto download + } + // have we checked recently for a new version? + if versionCheck.Timestamp > time.Now().Add(-24*time.Hour).Unix() { + return binPath, nil + } + // don't check for the next 24 hours + versionCheck.store(versionCheckPath) + if asset, err := getLatestVersion(brand); err == nil { + // no new version + if asset.version == string(versionCheck.CurrentVersion) { + return binPath, nil + } + } + } + +download: + asset, err := getLatestVersion(brand) + if err != nil { + if binExists { + // unable to get the latest version, but we already have a bin, use it + return binPath, nil + } + return "", err + } + if err := downloadAndExtract(asset, brand, binPath); err != nil { + return "", err + } + + versionCheck := versionCheck{CurrentVersion: asset.version} + if err := versionCheck.store(versionCheckPath); err != nil { + return "", err + } + return binPath, nil +} + +func getLatestVersion(brand CloudBrand) (*githubAsset, error) { + spinner := terminal.NewSpinner(terminal.Stderr) + spinner.Start() + defer spinner.Stop() + + resp, err := http.Get("https://api.github.com/repos/platformsh/cli/releases/latest") + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { + return nil, errors.New(http.StatusText(resp.StatusCode)) + } + manifestBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var manifest struct { + Name string + Assets []*githubAsset + } + if err := json.Unmarshal(manifestBody, &manifest); err != nil { + return nil, err + } + + info, err := osinfo.GetOSInfo() + if err != nil { + return nil, err + } + + var asset *githubAsset + for _, a := range manifest.Assets { + if !strings.HasSuffix(a.Name, ".gz") && !strings.HasSuffix(a.Name, ".zip") { + continue + } + if !strings.Contains(a.Name, brand.BinName) { + continue + } + if (strings.Contains(a.Name, info.Architecture) && strings.Contains(a.Name, info.Family)) || + (strings.Contains(a.Name, "all") && info.Family == "darwin") { + asset = a + break + } + } + if asset == nil { + return nil, errors.New(fmt.Sprintf("unable to find a suitable %s CLI tool for your machine (%s/%s)", brand, info.Family, info.Architecture)) + } + asset.version = manifest.Name + + return asset, nil +} + +func downloadAndExtract(asset *githubAsset, brand CloudBrand, binPath string) error { + resp, err := http.Get(asset.URL) + if err != nil { + return err + } + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { + return errors.New(http.StatusText(resp.StatusCode)) + } + + pr, pw := io.Pipe() + errs := make(chan error, 1) + go func() { + bar := progressbar.DefaultBytes(resp.ContentLength, fmt.Sprintf("Downloading %s CLI version %s", brand, asset.version)) + if _, err := io.Copy(io.MultiWriter(pw, bar), resp.Body); err != nil { + errs <- err + } + _ = bar.Close() + errs <- pw.Close() + }() + + gzr, err := gzip.NewReader(pr) + if err != nil { + return err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + for { + select { + case err := <-errs: + return err + default: + header, err := tr.Next() + switch { + case err == io.EOF: + return nil + case err != nil: + return err + case header == nil: + continue + default: + if header.Typeflag != tar.TypeReg { + continue + } + if header.Name != brand.BinName { + continue + } + if _, err := os.Stat(filepath.Dir(binPath)); os.IsNotExist(err) { + if err := os.MkdirAll(filepath.Dir(binPath), 0755); err != nil { + return err + } + } + out, err := os.OpenFile(binPath, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return err + } + if _, err := io.Copy(out, tr); err != nil { + out.Close() + return err + } + return out.Close() + } + } + } +} + +func loadVersionCheck(path string) *versionCheck { + data, err := os.ReadFile(path) + if err != nil { + return nil + } + var versionCheck versionCheck + if err := json.Unmarshal(data, &versionCheck); err != nil { + _ = os.Remove(path) + return nil + } + return &versionCheck +} + +func (versionCheck *versionCheck) store(path string) error { + versionCheck.Timestamp = time.Now().Unix() + data, err := json.Marshal(versionCheck) + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} diff --git a/local/platformsh/project.go b/local/platformsh/project.go index f96abe7c..cffd7464 100644 --- a/local/platformsh/project.go +++ b/local/platformsh/project.go @@ -22,9 +22,9 @@ package platformsh import ( goerr "errors" "fmt" - "io/ioutil" "os" "path/filepath" + "regexp" "github.com/pkg/errors" "github.com/symfony-cli/symfony-cli/git" @@ -32,8 +32,8 @@ import ( ) var ( - ErrProjectRootNotFoundNoGitRemote = goerr.New("project root not found, current directory not linked to a Platform.sh project") - ErrNoGitBranchMatching = goerr.New("current git branch name doesn't match any Platform.sh environments") + ErrProjectRootNotFoundNoGitRemote = goerr.New("project root not found, current directory not linked to a Platform.sh/Upsun project") + ErrNoGitBranchMatching = goerr.New("current git branch name doesn't match any Platform.sh/Upsun environments") ) type Project struct { @@ -43,13 +43,17 @@ type Project struct { } func ProjectFromDir(dir string, debug bool) (*Project, error) { - projectRoot, projectID := guessProjectRoot(dir, debug) + projectRoot := repositoryRootDir(dir) + if projectRoot == "" { + return nil, errors.New("unable to get project repository root") + } + projectID := getProjectID(projectRoot, debug) if projectID == "" { - return nil, errors.New("unable to get project root") + return nil, errors.New("unable to get project id") } - envID, err := potentialCurrentEnvironmentID(projectRoot) - if err != nil { - return nil, errors.Wrap(err, "unable to get current env") + envID := git.GetCurrentBranch(projectRoot) + if envID == "" { + return nil, errors.New("unable to get current env: unable to retrieve the current Git branch name") } app := GuessSelectedAppByDirectory(dir, FindLocalApplications(projectRoot)) if app == nil { @@ -68,21 +72,13 @@ func GetProjectRoot(debug bool) (string, error) { return "", errors.WithStack(err) } - if projectRoot, _ := guessProjectRoot(currentDir, debug); projectRoot != "" { + if projectRoot := repositoryRootDir(currentDir); projectRoot != "" { return projectRoot, nil } return "", errors.WithStack(ErrProjectRootNotFoundNoGitRemote) } -func potentialCurrentEnvironmentID(cwd string) (string, error) { - for _, potentialEnvironment := range guessCloudBranch(cwd) { - return potentialEnvironment, nil - } - - return "", errors.New("no known git upstream, branch or environment name") -} - func repositoryRootDir(currentDir string) string { for { f, err := os.Stat(filepath.Join(currentDir, ".git")) @@ -100,23 +96,24 @@ func repositoryRootDir(currentDir string) string { return "" } -func guessProjectRoot(currentDir string, debug bool) (string, string) { - rootDir := repositoryRootDir(currentDir) - if rootDir == "" { - return "", "" +func getProjectID(projectRoot string, debug bool) string { + brand := GuessCloudFromDirectory(projectRoot) + if brand == NoBrand { + return "" } - config := getProjectConfig(rootDir, debug) - if config == "" { - return "", "" + id := getProjectIDFromConfigFile(brand, projectRoot, debug) + if id != "" { + return id } - return rootDir, config + + return getProjectIDFromGitConfig(brand, projectRoot, debug) } -func getProjectConfig(projectRoot string, debug bool) string { - contents, err := ioutil.ReadFile(filepath.Join(projectRoot, ".platform", "local", "project.yaml")) +func getProjectIDFromConfigFile(brand CloudBrand, projectRoot string, debug bool) string { + contents, err := os.ReadFile(filepath.Join(projectRoot, brand.ProjectConfigPath, "local", "project.yaml")) if err != nil { if debug { - fmt.Fprintf(os.Stderr, "WARNING: unable to find Platform.sh config file: %s\n", err) + fmt.Fprintf(os.Stderr, "WARNING: unable to find %s config file: %s\n", brand, err) } return "" } @@ -125,25 +122,23 @@ func getProjectConfig(projectRoot string, debug bool) string { } if err := yaml.Unmarshal(contents, &config); err != nil { if debug { - fmt.Fprintf(os.Stderr, "ERROR: unable to decode Platform.sh config file: %s\n", err) + fmt.Fprintf(os.Stderr, "ERROR: unable to decode %s config file: %s\n", brand, err) } return "" } return config.ID } -func guessCloudBranch(cwd string) []string { - localBranch := git.GetCurrentBranch(cwd) - if localBranch == "" { - return []string{} +func getProjectIDFromGitConfig(brand CloudBrand, projectRoot string, debug bool) string { + for _, remote := range []string{brand.GitRemoteName, "origin"} { + url := git.GetRemoteURL(projectRoot, remote) + matches := regexp.MustCompile(`^([a-z0-9]{12,})@git\.`).FindStringSubmatch(url) + if len(matches) > 1 { + return string(matches[1]) + } } - - branches := []string{} - branches = append(branches, localBranch) - - if remoteBranch := git.GetUpstreamBranch(cwd, "origin", "upstream"); remoteBranch != "" { - branches = append(branches, remoteBranch) + if debug { + fmt.Fprintf(os.Stderr, "ERROR: unable to read the git config file\n") } - - return branches + return "" } diff --git a/local/platformsh/testdata/projectA/config/packages/doctrine.yaml b/local/platformsh/testdata/projectA/config/packages/doctrine.yaml new file mode 100644 index 00000000..4a05bd1c --- /dev/null +++ b/local/platformsh/testdata/projectA/config/packages/doctrine.yaml @@ -0,0 +1,3 @@ +doctrine: + dbal: + server_version: '%env(DATABASE_VERSION)%' diff --git a/local/process/listener.go b/local/process/listener.go index 531b2856..a5b21368 100644 --- a/local/process/listener.go +++ b/local/process/listener.go @@ -20,8 +20,8 @@ package process import ( + "fmt" "net" - "strconv" "github.com/pkg/errors" ) @@ -29,7 +29,7 @@ import ( // CreateListener creates a listener on a port // Pass a preferred port (will increment by 1 if port is not available) // or pass 0 to auto-find any available port -func CreateListener(port, preferredPort int) (net.Listener, int, error) { +func CreateListener(listenIp string, port, preferredPort int) (net.Listener, int, error) { var ln net.Listener var err error tryPort := preferredPort @@ -40,11 +40,11 @@ func CreateListener(port, preferredPort int) (net.Listener, int, error) { } for { // we really want to test availability on 127.0.0.1 - ln, err = net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(tryPort)) + ln, err = net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", tryPort)) if err == nil { ln.Close() // but then, we want to listen to as many local IP's as possible - ln, err = net.Listen("tcp", ":"+strconv.Itoa(tryPort)) + ln, err = net.Listen("tcp", fmt.Sprintf("%s:%d", listenIp, tryPort)) if err == nil { break } diff --git a/local/project/config.go b/local/project/config.go index bcc61d23..82049342 100644 --- a/local/project/config.go +++ b/local/project/config.go @@ -20,7 +20,6 @@ package project import ( - "io/ioutil" "os" "path/filepath" @@ -30,10 +29,13 @@ import ( "gopkg.in/yaml.v2" ) +const DockerComposeWorkerKey = "docker_compose" + // Config is the struct taken by New (should not be used for anything else) type Config struct { HomeDir string ProjectDir string + ListenIp string DocumentRoot string `yaml:"document_root"` Passthru string `yaml:"passthru"` Port int `yaml:"port"` @@ -41,13 +43,17 @@ type Config struct { PKCS12 string `yaml:"p12"` Logger zerolog.Logger AppVersion string - AllowHTTP bool `yaml:"allow_http"` - NoTLS bool `yaml:"no_tls"` - Daemon bool `yaml:"daemon"` + AllowHTTP bool `yaml:"allow_http"` + NoTLS bool `yaml:"no_tls"` + Daemon bool `yaml:"daemon"` + UseGzip bool `yaml:"use_gzip"` + TlsKeyLogFile string `yaml:"tls_key_log_file"` + NoWorkers bool `yaml:"no_workers"` + AllowCORS bool `yaml:"allow_cors"` } type FileConfig struct { - Proxy struct { + Proxy *struct { Domains []string `yaml:"domains"` } `yaml:"proxy"` HTTP *Config `yaml:"http"` @@ -79,6 +85,11 @@ func NewConfigFromContext(c *console.Context, projectDir string) (*Config, *File } config.AppVersion = c.App.Version config.ProjectDir = projectDir + if c.IsSet("allow-all-ip") { + config.ListenIp = "" + } else { + config.ListenIp = c.String("listen-ip") + } if c.IsSet("document-root") { config.DocumentRoot = c.String("document-root") } @@ -103,6 +114,19 @@ func NewConfigFromContext(c *console.Context, projectDir string) (*Config, *File if c.IsSet("daemon") { config.Daemon = c.Bool("daemon") } + if c.IsSet("use-gzip") { + config.UseGzip = c.Bool("use-gzip") + } + if c.IsSet("tls-key-log-file") { + config.TlsKeyLogFile = c.String("tls-key-log-file") + } + if c.IsSet("no-workers") { + config.NoWorkers = c.Bool("no-workers") + } + if c.IsSet("allow-cors") { + config.AllowCORS = c.Bool("allow-cors") + } + return config, fileConfig, nil } @@ -112,7 +136,7 @@ func newConfigFromFile(configFile string) (*FileConfig, error) { return nil, nil } - contents, err := ioutil.ReadFile(configFile) + contents, err := os.ReadFile(configFile) if err != nil { return nil, err } @@ -135,6 +159,17 @@ func (c *FileConfig) parseWorkers() error { return nil } + if v, ok := c.Workers[DockerComposeWorkerKey]; ok && v == nil { + c.Workers[DockerComposeWorkerKey] = &Worker{ + Cmd: []string{"docker", "compose", "up"}, + Watch: []string{ + "compose.yaml", "compose.override.yaml", + "compose.yml", "compose.override.yml", + "docker-compose.yml", "docker-compose.override.yml", + "docker-compose.yaml", "docker-compose.override.yaml", + }, + } + } if v, ok := c.Workers["yarn_encore_watch"]; ok && v == nil { c.Workers["yarn_encore_watch"] = &Worker{ Cmd: []string{"yarn", "encore", "dev", "--watch"}, diff --git a/local/project/project.go b/local/project/project.go index cd099862..ac179b12 100644 --- a/local/project/project.go +++ b/local/project/project.go @@ -21,7 +21,6 @@ package project import ( "encoding/json" - "io/ioutil" "net/http" "os" "path/filepath" @@ -35,11 +34,9 @@ import ( // Project represents a PHP project type Project struct { - HTTP *lhttp.Server - PHPServer *php.Server - Logger zerolog.Logger - homeDir string - projectDir string + HTTP *lhttp.Server + PHPServer *php.Server + Logger zerolog.Logger } // New creates a new PHP project @@ -50,17 +47,19 @@ func New(c *Config) (*Project, error) { } passthru, err := realPassthru(documentRoot, c.Passthru) p := &Project{ - Logger: c.Logger.With().Str("source", "HTTP").Logger(), - homeDir: c.HomeDir, - projectDir: c.ProjectDir, + Logger: c.Logger.With().Str("source", "HTTP").Logger(), HTTP: &lhttp.Server{ DocumentRoot: documentRoot, Port: c.Port, PreferredPort: c.PreferredPort, + ListenIp: c.ListenIp, Logger: c.Logger, PKCS12: c.PKCS12, AllowHTTP: c.AllowHTTP, + UseGzip: c.UseGzip, Appversion: c.AppVersion, + TlsKeyLogFile: c.TlsKeyLogFile, + AllowCORS: c.AllowCORS, }, } if err != nil { @@ -75,7 +74,7 @@ func New(c *Config) (*Project, error) { return nil } } else { - p.PHPServer, err = php.NewServer(c.HomeDir, c.ProjectDir, documentRoot, passthru, c.Logger) + p.PHPServer, err = php.NewServer(c.HomeDir, c.ProjectDir, documentRoot, passthru, c.AppVersion, c.Logger) if err != nil { return nil, err } @@ -109,8 +108,8 @@ func realPassthru(documentRoot, passthru string) (string, error) { } func guessDocumentRoot(path string) string { - // for Symfony: check if public-dir is setup in composer.json first - if b, err := ioutil.ReadFile(filepath.Join(path, "composer.json")); err == nil { + // for Symfony: check if public-dir is set up in composer.json first + if b, err := os.ReadFile(filepath.Join(path, "composer.json")); err == nil { var f map[string]interface{} if err := json.Unmarshal(b, &f); err == nil { if f1, ok := f["extra"]; ok { diff --git a/local/projects/configured_test.go b/local/projects/configured_test.go new file mode 100644 index 00000000..afe0facc --- /dev/null +++ b/local/projects/configured_test.go @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package projects + +import ( + "testing" +) + +func TestGetConfiguredAndRunning(t *testing.T) { + proxyProjects := map[string]*ConfiguredProject{ + "~/app1": { + Scheme: "https", + Port: 8000, + }, + "/var/www/app2": { + Scheme: "http", + Port: 8001, + }, + } + + runningProjects := map[string]*ConfiguredProject{ + "~/app1": { + Scheme: "https", + Port: 8000, + }, + "/var/www/app2": { + Scheme: "http", + Port: 8001, + }, + "/var/www/app3": { + Scheme: "ftp", + Port: 8002, + }, + } + + projects, err := GetConfiguredAndRunning(proxyProjects, runningProjects) + if err != nil { + t.Errorf("Error was not expected: %v", err) + } + + if len(projects) != 3 { + t.Errorf("Expected 2 projects, got %d", len(projects)) + } + + if projects["~/app1"].Port != 8000 { + t.Errorf("Expected 8000, got %d", projects["~/app1"].Port) + } + + if projects["~/app1"].Scheme != "https" { + t.Errorf("Expected \"https\", got %s", projects["~/app1"].Scheme) + } + + if projects["/var/www/app2"].Port != 8001 { + t.Errorf("Expected 8001, got %d", projects["/var/www/app2"].Port) + } + + if projects["/var/www/app2"].Scheme != "http" { + t.Errorf("Expected \"http\", got %s", projects["/var/www/app2"].Scheme) + } + + if projects["/var/www/app3"].Port != 8002 { + t.Errorf("Expected 8002, got %d", projects["/var/www/app3"].Port) + } + + if projects["/var/www/app3"].Scheme != "ftp" { + t.Errorf("Expected \"ftp\", got %s", projects["/var/www/app3"].Scheme) + } +} diff --git a/local/proxy/cert_store.go b/local/proxy/cert_store.go index b015d89b..29002d95 100644 --- a/local/proxy/cert_store.go +++ b/local/proxy/cert_store.go @@ -23,7 +23,7 @@ import ( "crypto/tls" "sync" - lru "github.com/hashicorp/golang-lru" + "github.com/hashicorp/golang-lru/arc/v2" "github.com/symfony-cli/cert" ) @@ -31,12 +31,12 @@ type certStore struct { proxyCfg *Config ca *cert.CA lock sync.Mutex - cache *lru.ARCCache + cache *arc.ARCCache[string, tls.Certificate] } // newCertStore creates a store to keep SSL certificates in memory func (p *Proxy) newCertStore(ca *cert.CA) *certStore { - cache, _ := lru.NewARC(1024) + cache, _ := arc.NewARC[string, tls.Certificate](1024) return &certStore{ proxyCfg: p.Config, ca: ca, @@ -50,7 +50,7 @@ func (c *certStore) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certi defer c.lock.Unlock() name := c.proxyCfg.NormalizeDomain(clientHello.ServerName) if val, ok := c.cache.Get(name); ok { - cert := val.(tls.Certificate) + cert := val return &cert, nil } cert, err := c.ca.CreateCert([]string{name}) diff --git a/local/proxy/config.go b/local/proxy/config.go index 04c3831b..d5db1627 100644 --- a/local/proxy/config.go +++ b/local/proxy/config.go @@ -22,7 +22,6 @@ package proxy import ( "encoding/json" "fmt" - "io/ioutil" "log" "net/http" "os" @@ -66,11 +65,11 @@ func Load(homeDir string) (*Config, error) { if err := os.MkdirAll(filepath.Dir(proxyFile), 0755); err != nil { return nil, errors.Wrapf(err, "unable to create directory for %s", proxyFile) } - if err := ioutil.WriteFile(proxyFile, DefaultConfig, 0644); err != nil { + if err := os.WriteFile(proxyFile, DefaultConfig, 0644); err != nil { return nil, errors.Wrapf(err, "unable to write %s", proxyFile) } } - data, err := ioutil.ReadFile(proxyFile) + data, err := os.ReadFile(proxyFile) if err != nil { return nil, errors.Wrapf(err, "unable to read the proxy configuration file, %s", proxyFile) } @@ -159,6 +158,23 @@ func (c *Config) GetDomains(dir string) []string { return domains } +func (c *Config) GetReachableDomains(dir string) []string { + c.mu.Lock() + defer c.mu.Unlock() + domains := []string{} + for domain, d := range c.domains { + // domain is defined using a wildcard: we don't know the exact domain, + // so we can't use it directly as-is to reach the project + if strings.Contains(domain, "*") { + continue + } + if d == dir { + domains = append(domains, domain+"."+c.TLD) + } + } + return domains +} + func (c *Config) SetDomains(domains map[string]string) { c.mu.Lock() c.domains = domains @@ -221,7 +237,7 @@ func (c *Config) Watch() { // reloads the TLD and the domains (not the port) func (c *Config) reload() { - data, err := ioutil.ReadFile(c.path) + data, err := os.ReadFile(c.path) if err != nil { return } @@ -249,7 +265,7 @@ func (c *Config) Save() error { if err != nil { return errors.WithStack(err) } - return errors.WithStack(ioutil.WriteFile(c.path, data, 0644)) + return errors.WithStack(os.WriteFile(c.path, data, 0644)) } // should be called with a lock a place diff --git a/local/proxy/config_test.go b/local/proxy/config_test.go index 73df9628..ebad7bf3 100644 --- a/local/proxy/config_test.go +++ b/local/proxy/config_test.go @@ -43,3 +43,16 @@ func (s *ProxySuite) TestGetDir(c *C) { c.Assert(p.GetDir("foo.symfony.com"), Equals, "any_symfony_com") c.Assert(p.GetDir("foo.live.symfony.com"), Equals, "any_live_symfony_com") } + +func (s *ProxySuite) TestGetReachableDomains(c *C) { + p := &Config{ + TLD: "wip", + domains: map[string]string{ + "*.symfony": "symfony_com", + "symfony": "symfony_com", + "custom.*.symfony": "symfony_com", + "*.live.symfony": "symfony_com", + }, + } + c.Assert(p.GetReachableDomains("symfony_com"), DeepEquals, []string{"symfony.wip"}) +} diff --git a/local/proxy/proxy.go b/local/proxy/proxy.go index 2e7c4831..50bfc571 100644 --- a/local/proxy/proxy.go +++ b/local/proxy/proxy.go @@ -54,12 +54,12 @@ func tlsToLocalWebServer(proxy *goproxy.ProxyHttpServer, tlsConfig *tls.Config, ctx.Warnf("Error closing client connection: %s", err) } } - connectDial := func(proxy *goproxy.ProxyHttpServer, network, addr string) (c net.Conn, err error) { - if proxy.ConnectDial != nil { + connectDial := func(ctx *goproxy.ProxyCtx, network, addr string) (c net.Conn, err error) { + if ctx.Proxy.ConnectDial != nil { return proxy.ConnectDial(network, addr) } - if proxy.Tr.Dial != nil { - return proxy.Tr.Dial(network, addr) + if ctx.Proxy.Tr.DialContext != nil { + return proxy.Tr.DialContext(ctx.Req.Context(), network, addr) } return net.Dial(network, addr) } @@ -91,10 +91,12 @@ func tlsToLocalWebServer(proxy *goproxy.ProxyHttpServer, tlsConfig *tls.Config, } ctx.Logf("Assuming CONNECT is TLS, TLS proxying it") - targetSiteCon, err := connectDial(proxy, "tcp", fmt.Sprintf("127.0.0.1:%d", localPort)) + targetSiteCon, err := connectDial(ctx, "tcp", fmt.Sprintf("127.0.0.1:%d", localPort)) if err != nil { - targetSiteCon.Close() httpError(proxyClientTls, ctx, err) + if targetSiteCon != nil { + targetSiteCon.Close() + } return } @@ -226,7 +228,7 @@ func New(config *Config, ca *cert.CA, logger *log.Logger, debug bool) *Proxy { backend := fmt.Sprintf("127.0.0.1:%d", pid.Port) if hostPort != "443" { - // No TLS termination required, let's go trough regular proxy + // No TLS termination required, let's go through regular proxy return goproxy.OkConnect, backend } @@ -313,16 +315,27 @@ func (p *Proxy) Start() error { } func (p *Proxy) servePacFile(w http.ResponseWriter, r *http.Request) { + // Use the current request hostname (r.Host) to generate the PAC file. + // This means that as soon as you are able to reach the proxy, the generated + // PAC file will expose an appropriate hostname or IP even if the proxy + // is running remotely, in a container or a VM. + // No need to fall back to p.Host and p.Port as r.Host is already checked + // upper in the stacktrace. w.Header().Add("Content-Type", "application/x-ns-proxy-autoconfig") w.Write([]byte(fmt.Sprintf(`// Only proxy *.%s requests // Configuration file in ~/.symfony5/proxy.json function FindProxyForURL (url, host) { if (dnsDomainIs(host, '.%s')) { - return 'PROXY %s:%d'; + if (isResolvable(host)) { + return 'DIRECT'; + } + + return 'PROXY %s'; } + return 'DIRECT'; } -`, p.TLD, p.TLD, p.Host, p.Port))) +`, p.TLD, p.TLD, r.Host))) } func (p *Proxy) serveIndex(w http.ResponseWriter, r *http.Request) { @@ -332,7 +345,7 @@ func (p *Proxy) serveIndex(w http.ResponseWriter, r *http.Request) { if err != nil { return } - runningProjects, err := pid.ToConfiguredProjects() + runningProjects, err := pid.ToConfiguredProjects(true) if err != nil { return } diff --git a/local/proxy/proxy_test.go b/local/proxy/proxy_test.go index 03437d05..55215193 100644 --- a/local/proxy/proxy_test.go +++ b/local/proxy/proxy_test.go @@ -22,7 +22,7 @@ package proxy import ( "crypto/tls" "crypto/x509" - "io/ioutil" + "io" "log" "net/http" "net/http/httptest" @@ -114,7 +114,7 @@ func (s *ProxySuite) TestProxy(c *C) { Timeout: 1 * time.Second, } - // Test proxying a request to a non registered project + // Test proxying a request to a non-registered project { req, _ := http.NewRequest("GET", "https://foo.wip/", nil) req.Close = true @@ -122,7 +122,7 @@ func (s *ProxySuite) TestProxy(c *C) { res, err := client.Do(req) c.Assert(err, IsNil) c.Assert(res.StatusCode, Equals, http.StatusNotFound) - body, _ := ioutil.ReadAll(res.Body) + body, _ := io.ReadAll(res.Body) c.Check(strings.Contains(string(body), "not linked"), Equals, true) } @@ -134,7 +134,7 @@ func (s *ProxySuite) TestProxy(c *C) { res, err := client.Do(req) c.Assert(err, IsNil) c.Assert(res.StatusCode, Equals, http.StatusNotFound) - body, _ := ioutil.ReadAll(res.Body) + body, _ := io.ReadAll(res.Body) c.Check(strings.Contains(string(body), "not started"), Equals, true) } /* @@ -164,7 +164,7 @@ func (s *ProxySuite) TestProxy(c *C) { res, err := client.Do(req) c.Assert(err, IsNil) c.Assert(res.StatusCode, Equals, http.StatusOK) - body, _ := ioutil.ReadAll(res.Body) + body, _ := io.ReadAll(res.Body) c.Check(string(body), Equals, "symfony.wip") } */ @@ -187,7 +187,7 @@ func (s *ProxySuite) TestProxy(c *C) { res, err := client.Do(req) c.Assert(err, IsNil) - body, _ := ioutil.ReadAll(res.Body) + body, _ := io.ReadAll(res.Body) c.Assert(res.StatusCode, Equals, http.StatusOK) c.Assert(string(body), Equals, "http://symfony-no-tls.wip") } diff --git a/local/runner.go b/local/runner.go index b128c91d..aa24bdc2 100644 --- a/local/runner.go +++ b/local/runner.go @@ -32,6 +32,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/rs/zerolog" "github.com/symfony-cli/console" "github.com/symfony-cli/symfony-cli/inotify" "github.com/symfony-cli/symfony-cli/local/pid" @@ -60,6 +61,7 @@ type Runner struct { pidFile *pid.PidFile BuildCmdHook func(*exec.Cmd) error + SuccessHook func(*Runner, *exec.Cmd) AlwaysRestartOnExit bool } @@ -79,6 +81,12 @@ func NewRunner(pidFile *pid.PidFile, mode runnerMode) (*Runner, error) { } func (r *Runner) Run() error { + lw, err := r.pidFile.LogWriter() + if err != nil { + return err + } + logger := zerolog.New(lw).With().Str("source", "runner").Str("cmd", r.pidFile.String()).Timestamp().Logger() + if r.mode == RunnerModeLoopDetached { if !reexec.IsChild() { varDir := filepath.Join(util.GetHomeDir(), "var") @@ -107,7 +115,7 @@ func (r *Runner) Run() error { cmdExitChan := make(chan error) // receives command exit status, allow to cmd.Wait() in non-blocking way restartChan := make(chan bool) // receives requests to restart the command sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Kill, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + signal.Notify(sigChan, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM) defer signal.Stop(sigChan) if len(r.pidFile.Watched) > 0 { @@ -125,7 +133,7 @@ func (r *Runner) Run() error { continue } - terminal.Logger.Debug().Msg("Got event: " + event.Event().String()) + logger.Debug().Msg("Got event: " + event.Event().String()) select { case restartChan <- true: @@ -138,10 +146,10 @@ func (r *Runner) Run() error { if fi, err := os.Stat(watched); err != nil { continue } else if fi.IsDir() { - terminal.Logger.Info().Msg("Watching directory " + watched) + logger.Info().Msg("Watching directory " + watched) watched = filepath.Join(watched, "...") } else { - terminal.Logger.Info().Msg("Watching file " + watched) + logger.Info().Msg("Watching file " + watched) } if err := inotify.Watch(watched, c, inotify.All); err != nil { return errors.Wrapf(err, `could not watch "%s"`, watched) @@ -177,19 +185,21 @@ func (r *Runner) Run() error { if r.mode == RunnerModeLoopDetached { reexec.NotifyForeground("started") } - + logger.Debug().Msg("Waiting for channels (first boot)") select { case err := <-cmdExitChan: + logger.Debug().Msg("Received exit (first boot)") if err != nil { return errors.Wrapf(err, `command "%s" failed early`, r.pidFile) } timer.Stop() // when the command is really fast to run, it will be already - // done here so we need to forward exit status as it has + // done here, so we need to forward exit status as if it has // finished later one go func() { cmdExitChan <- err }() case <-timer.C: + logger.Debug().Msg("Received timer message (first boot)") } } @@ -210,41 +220,58 @@ func (r *Runner) Run() error { select { case sig := <-sigChan: - terminal.Logger.Info().Msgf("Signal \"%s\" received, forwarding to command and exiting\n", sig) + logger.Info().Msgf("Signal \"%s\" received, forwarding and exiting\n", sig) err := cmd.Process.Signal(sig) if err != nil && runtime.GOOS == "windows" && strings.Contains(err.Error(), "not supported by windows") { return exec.Command("CMD", "/C", "TASKKILL", "/F", "/PID", strconv.Itoa(cmd.Process.Pid)).Run() } return err case <-restartChan: + logger.Debug().Msg("Received restart") // We use SIGTERM here because it's nicer and thus when we use our // wrappers, signal will be nicely forwarded cmd.Process.Signal(syscall.SIGTERM) // we need to drain cmdExit channel to unblock cmd channel receiver <-cmdExitChan + // Command exited case err := <-cmdExitChan: - err = errors.Wrapf(err, `command "%s" failed`, r.pidFile) + logger.Debug().Msg("Received exit") + if err == nil && r.SuccessHook != nil { + logger.Debug().Msg("Running success hook") + r.SuccessHook(r, cmd) + } - if looping { - // Command exited, let's wait for a change or 5 seconds to restart the command or a signal to exit + // Command is NOT set up to loop, stop here and remove the pidFile + // if the command is successful + if !looping { if err != nil { - terminal.Logger.Error().Msgf("%s, waiting 5 seconds before restarting it", err) - timer.Reset(5 * time.Second) - } else if r.AlwaysRestartOnExit { - terminal.Logger.Error().Msgf(`command "%s" exited, restarting it immediately`, r.pidFile) + logger.Debug().Msg("Not looping, exiting with error") + return err } - continue + logger.Debug().Msg("Removing pid file") + return r.pidFile.Remove() } - if err == nil { - return r.pidFile.Remove() + // Command is set up to restart on exit (usually PHP builtin server) + if r.AlwaysRestartOnExit { + logger.Debug().Msg("Looping") + // In case of error we want to wait up-to 5 seconds before + // restarting the command, this avoids overloading the system with a + // failing command + if err != nil { + logger.Error().Msgf(`command exited: %s, waiting 5 seconds before restarting it`, err) + timer.Reset(5 * time.Second) + } else { + logger.Error().Msg("command exited, restarting it immediately") + } + continue } - return err + return nil } - terminal.Logger.Info().Msgf(`Restarting command "%s"`, r.pidFile) + logger.Info().Msg("Restarting command") } } @@ -253,6 +280,10 @@ func (r *Runner) buildCmd() (*exec.Cmd, error) { cmd.Env = os.Environ() cmd.Dir = r.pidFile.Dir + if err := buildCmd(cmd, r.mode == RunnerModeOnce && terminal.Stdin.IsInteractive()); err != nil { + return nil, err + } + if r.mode == RunnerModeOnce { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/local/runner_posix.go b/local/runner_posix.go new file mode 100644 index 00000000..80af5da5 --- /dev/null +++ b/local/runner_posix.go @@ -0,0 +1,39 @@ +//go:build !windows +// +build !windows + +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package local + +import ( + "os/exec" + "syscall" +) + +func buildCmd(cmd *exec.Cmd, foreground bool) error { + cmd.SysProcAttr = &syscall.SysProcAttr{ + // isolate each command in a new process group that we can cleanly send + // signal to when we want to stop it + Setpgid: true, + Foreground: foreground, + } + + return nil +} diff --git a/local/runner_windows.go b/local/runner_windows.go new file mode 100644 index 00000000..868cae6c --- /dev/null +++ b/local/runner_windows.go @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package local + +import "os/exec" + +func buildCmd(*exec.Cmd, bool) error { + return nil +} diff --git a/main.go b/main.go index 97e9f298..6497ce2b 100644 --- a/main.go +++ b/main.go @@ -23,14 +23,13 @@ package main import ( "fmt" - "io/ioutil" "os" "time" - "github.com/rs/zerolog" "github.com/symfony-cli/console" "github.com/symfony-cli/symfony-cli/commands" "github.com/symfony-cli/symfony-cli/local/php" + "github.com/symfony-cli/symfony-cli/local/platformsh" "github.com/symfony-cli/terminal" ) @@ -43,7 +42,18 @@ var ( buildDate string ) +func getCliExtraEnv() []string { + return []string{ + "SYMFONY_CLI_VERSION=" + version, + "SYMFONY_CLI_BINARY_NAME=" + console.CurrentBinaryName(), + } +} + func main() { + if os.Getenv("SC_DEBUG") == "1" { + terminal.SetLogLevel(5) + } + args := os.Args name := console.CurrentBinaryName() // called via "php"? @@ -55,28 +65,33 @@ func main() { // called via "symfony php"? if len(args) >= 2 && php.IsBinaryName(args[1]) { e := &php.Executor{ - BinName: args[1], - Args: args[1:], + BinName: args[1], + Args: args[1:], + ExtraEnv: getCliExtraEnv(), + Logger: terminal.Logger, } os.Exit(e.Execute(true)) } // called via "symfony console"? if len(args) >= 2 && args[1] == "console" { - args[1] = "bin/console" - if _, err := os.Stat("app/console"); err == nil { - args[1] = "app/console" + if executor, err := php.SymfonyConsoleExecutor(terminal.Logger, args[2:]); err == nil { + executor.ExtraEnv = getCliExtraEnv() + os.Exit(executor.Execute(false)) } - e := &php.Executor{ - BinName: "php", - Args: args, - } - os.Exit(e.Execute(false)) } - // called via "symfony composer"? - if len(args) >= 2 && args[1] == "composer" { - res := php.Composer("", args[2:], []string{}, os.Stdout, os.Stderr, ioutil.Discard, zerolog.Nop()) - terminal.Eprintln(res.Error()) - os.Exit(res.ExitCode()) + // called via "symfony composer" or "symfony pie"? + if len(args) >= 2 { + if args[1] == "composer" { + res := php.Composer("", args[2:], getCliExtraEnv(), os.Stdout, os.Stderr, os.Stderr, terminal.Logger) + terminal.Eprintln(res.Error()) + os.Exit(res.ExitCode()) + } + + if args[1] == "pie" { + res := php.Pie("", args[2:], getCliExtraEnv(), os.Stdout, os.Stderr, os.Stderr, terminal.Logger) + terminal.Eprintln(res.Error()) + os.Exit(res.ExitCode()) + } } for _, env := range []string{"BRANCH", "ENV", "APPLICATION_NAME"} { @@ -91,7 +106,7 @@ func main() { } cmds := commands.CommonCommands() - psh, err := commands.GetPSH() + psh, err := platformsh.Get() if err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) @@ -101,7 +116,7 @@ func main() { app := &console.Application{ Name: "Symfony CLI", Usage: "Symfony CLI helps developers manage projects, from local code to remote infrastructure", - Copyright: fmt.Sprintf("(c) 2017-%d Symfony SAS #StandWithUkraine Support Ukraine", time.Now().Year()), + Copyright: fmt.Sprintf("(c) 2021-%d Fabien Potencier", time.Now().Year()), FlagEnvPrefix: []string{"SYMFONY", "PLATFORM"}, Commands: cmds, Action: func(ctx *console.Context) error { diff --git a/reexec/reexec.go b/reexec/reexec.go index 15e28619..b1649672 100644 --- a/reexec/reexec.go +++ b/reexec/reexec.go @@ -21,7 +21,6 @@ package reexec import ( "fmt" - "io/ioutil" "os" "os/exec" "os/signal" @@ -30,11 +29,11 @@ import ( "time" "github.com/pkg/errors" + "github.com/rjeczalik/notify" "github.com/symfony-cli/console" "github.com/symfony-cli/symfony-cli/inotify" "github.com/symfony-cli/symfony-cli/util" "github.com/symfony-cli/terminal" - "github.com/syncthing/notify" ) const UP = "up" @@ -86,11 +85,12 @@ func ExecBinaryWithEnv(binary string, envs []string) bool { } func Background(homeDir string) error { + terminal.Logger.Debug().Str("source", "reexec").Msg("let's go to the background!") if util.IsGoRun() { return errors.New("Not applicable in a Go run context") } - statusFile, err := ioutil.TempFile(homeDir, "status-") + statusFile, err := os.CreateTemp(homeDir, "status-") if err != nil { return errors.Wrap(err, "Could not create status file") } @@ -109,15 +109,14 @@ func Background(homeDir string) error { os.Setenv("REEXEC_WATCH_PID", fmt.Sprint(Getppid())) // We are in a reexec.Restart call (probably after an upgrade), watch // REEXEC_SHELL_PID instead of direct parent. - // For interactive sessions, this is not an issue, but for long running - // processes like tunnel:open, if we don't do that, they will exits right + // For interactive sessions, this is not an issue, but for long-running + // processes like tunnel:open, if we don't do that, they will exit right // after returning back to the shell because the direct parent (the initial // process that got upgraded) is the one watched. if shellPID := os.Getenv("REEXEC_SHELL_PID"); shellPID != "" { os.Setenv("REEXEC_WATCH_PID", shellPID) } - terminal.Logger.Debug().Msg("Let's go to the background!") p, err := Respawn() if err != nil { return errors.Wrap(err, "Could not respawn") @@ -158,14 +157,13 @@ func Background(homeDir string) error { // end-up receiving a status in statusCh so no particular // processing to do here. case event := <-watcherChan: - terminal.Logger.Info().Msg("FS event received: " + event.Event().String()) + terminal.Logger.Debug().Str("source", "reexec").Msg("FS event received: " + event.Event().String()) if event.Event() == notify.Remove { return nil } if event.Event() == notify.Write { ticker.Stop() - break } case status := <-statusCh: return console.Exit("", status) @@ -177,6 +175,7 @@ func Background(homeDir string) error { } func NotifyForeground(status string) error { + terminal.Logger.Debug().Str("source", "reexec").Msg("notify foreground") if !IsChild() { return nil } @@ -192,10 +191,11 @@ func NotifyForeground(status string) error { os.Stderr.Close() return os.Remove(statusFile) } - return ioutil.WriteFile(statusFile, []byte(status), 0600) + return os.WriteFile(statusFile, []byte(status), 0600) } func WatchParent(stopCh chan bool) error { + terminal.Logger.Debug().Str("source", "reexec").Msg("watch parent") spid := os.Getenv("REEXEC_WATCH_PID") if spid == "" { return nil @@ -224,6 +224,7 @@ func WatchParent(stopCh chan bool) error { } func Restart(postRespawn func()) error { + terminal.Logger.Debug().Str("source", "reexec").Msg("restart") if err := os.Setenv("REEXEC_PPID", fmt.Sprint(os.Getpid())); nil != err { return errors.WithStack(err) } @@ -281,6 +282,7 @@ func Restart(postRespawn func()) error { } func Respawn() (*os.Process, error) { + terminal.Logger.Debug().Str("source", "reexec").Msg("respawn") argv0, err := console.CurrentBinaryPath() if err != nil { return nil, err diff --git a/symfony/cli.go b/symfony/cli.go new file mode 100644 index 00000000..b7130a25 --- /dev/null +++ b/symfony/cli.go @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package symfony + +import ( + "bytes" + "encoding/json" + "fmt" + "os/exec" + "strings" + + "github.com/pkg/errors" + "github.com/symfony-cli/symfony-cli/local/php" +) + +type CliApp struct { + Commands []CliCommand + Namespaces []CliNamespace +} + +type CliNamespace struct { + ID string + Commands []string +} + +type CliCommand struct { + Name string + Usage []string + Description string + Help string + Definition CliDefinition + Hidden bool + Aliases []string +} + +type CliDefinition struct { + Arguments map[string]CliArgument + Options map[string]CliOption +} + +type CliArgument struct { + Required bool `json:"is_required"` + IsArray bool `json:"is_array"` + Description string `json:"description"` + Default interface{} `json:"default"` +} + +type CliOption struct { + Shortcut string `json:"shortcut"` + Description string `json:"description"` + AcceptValue bool `json:"accept_value"` + IsValueRequired bool `json:"is_value_required"` + IsMultiple bool `json:"is_multiple"` + Default interface{} `json:"default"` +} + +func NewCliApp(projectDir string, args []string) (*CliApp, error) { + args = append(args, "list", "--format=json") + var buf bytes.Buffer + e := &php.Executor{ + BinName: "php", + Dir: projectDir, + Args: args, + Stdout: &buf, + Stderr: &buf, + } + if ret := e.Execute(false); ret != 0 { + return nil, errors.Errorf("unable to list commands (%s):\n%s", strings.Join(args, " "), buf.String()) + } + return parseCommands(buf.Bytes()) +} + +func NewGoCliApp(projectDir string, binPath string, args []string) (*CliApp, error) { + var buf bytes.Buffer + cmd := exec.Command(binPath, "list", "--format=json") + cmd.Args = append(cmd.Args, args...) + fmt.Println(cmd.Args) + cmd.Dir = projectDir + cmd.Stdout = &buf + cmd.Stderr = &buf + if err := cmd.Run(); err != nil { + return nil, errors.Errorf("unable to list commands (%s):\n%s\n%s", strings.Join(args, " "), err, buf.String()) + } + return parseCommands(buf.Bytes()) +} + +func parseCommands(output []byte) (*CliApp, error) { + // Fix PHP types + cleanOutput := bytes.ReplaceAll(output, []byte(`"arguments":[]`), []byte(`"arguments":{}`)) + var app *CliApp + if err := json.Unmarshal(cleanOutput, &app); err != nil { + return nil, err + } + return app, nil +} diff --git a/updater/updater.go b/updater/updater.go index 483c69c4..5aa66f1f 100644 --- a/updater/updater.go +++ b/updater/updater.go @@ -23,7 +23,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "os" "path/filepath" @@ -96,8 +95,8 @@ func (updater *Updater) CheckForNewVersion(currentVersionStr string) { case <-updater.timer.C: updater.logger.Printf("Checking for updates timeout expired") case newVersionFound := <-newVersionCh: + updater.silence() if newVersionFound == nil { - updater.silence() return } fmt.Fprintf(updater.Output, "\n INFO A new Symfony CLI version is available (%s, currently running %s).\n\n", newVersionFound, currentVersion) @@ -132,7 +131,7 @@ func (updater *Updater) check(currentVersion *version.Version, enableCache bool) if enableCache && manifestFileErr == nil { if stat, err := manifestFile.Stat(); err == nil { if time.Since(stat.ModTime()) < 1*time.Hour { - if manifestCacheBody, manifestCacheErr := ioutil.ReadAll(manifestFile); manifestCacheErr == nil { + if manifestCacheBody, manifestCacheErr := io.ReadAll(manifestFile); manifestCacheErr == nil { manifestBody = manifestCacheBody } } else { @@ -153,7 +152,7 @@ func (updater *Updater) check(currentVersion *version.Version, enableCache bool) updater.logger.Printf("Checking for updates (current version: %s)", currentVersion) if manifestBody == nil { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://api.github.com/repos/symfony-cli/symfony-cli/releases/latest"), nil) + req, err := http.NewRequest(http.MethodGet, "https://api.github.com/repos/symfony-cli/symfony-cli/releases/latest", nil) if err != nil { updater.logger.Err(err).Msg("") return nil @@ -171,13 +170,13 @@ func (updater *Updater) check(currentVersion *version.Version, enableCache bool) return nil } - manifestBody, err = ioutil.ReadAll(resp.Body) + manifestBody, err = io.ReadAll(resp.Body) if err != nil { updater.logger.Err(err).Msg("") return nil } - if err := ioutil.WriteFile(manifestCachePath, manifestBody, 0644); err != nil { + if err := os.WriteFile(manifestCachePath, manifestBody, 0644); err != nil { updater.logger.Err(err).Msg("") return nil }