├── .dockerignore ├── .gitattributes ├── .github └── workflows │ └── go.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── AUTHORS ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docs ├── Makefile ├── cmds.json └── source │ ├── conf.py │ ├── index.rst │ ├── installing.rst │ └── reference.rst ├── go.mod ├── go.sum ├── install.sh ├── misc ├── bash-completion ├── build-all.sh ├── build_publish_to_packagecloud.sh ├── check-all-cmds-docs.sh ├── check-license.sh ├── generate_metadata.sh ├── install.sh └── zsh-completion ├── plugins.md ├── requirements.txt └── tsuru ├── admin ├── app.go ├── app_test.go ├── broker.go ├── broker_test.go ├── cluster.go ├── cluster_test.go ├── event.go ├── event_test.go ├── plan.go ├── plan_test.go ├── platform.go ├── platform_test.go ├── pool.go ├── pool_test.go ├── quota.go ├── quota_test.go ├── services.go ├── services_test.go ├── suite_test.go └── testdata │ ├── Dockerfile │ ├── cacert.pem │ ├── cert.pem │ ├── doc.md │ ├── key.pem │ └── manifest.yml ├── app ├── app.go └── app_test.go ├── auth ├── login.go ├── login_test.go ├── logout.go ├── logout_test.go ├── native.go ├── oauth.go ├── oauth_test.go ├── oidc.go ├── oidc_test.go ├── open.go ├── open_darwin.go ├── open_test.go ├── open_windows.go └── suite_test.go ├── client ├── apps.go ├── apps_test.go ├── apps_type.go ├── archiver.go ├── archiver_test.go ├── archiver_unix_test.go ├── auth.go ├── auth_test.go ├── autoscale.go ├── autoscale_test.go ├── build.go ├── build_test.go ├── certificate.go ├── certificate_test.go ├── deploy.go ├── deploy_test.go ├── doc.go ├── env.go ├── env_test.go ├── event.go ├── event_test.go ├── executor.go ├── executor_test.go ├── executor_windows.go ├── flags.go ├── flags_test.go ├── init.go ├── init_test.go ├── job_or_app.go ├── jobs.go ├── jobs_test.go ├── log.go ├── log_test.go ├── metadata.go ├── metadata_test.go ├── permission.go ├── permission_test.go ├── plan.go ├── plan_test.go ├── plugin.go ├── plugin_test.go ├── pool.go ├── pool_test.go ├── router.go ├── router_test.go ├── run.go ├── run_test.go ├── services.go ├── services_test.go ├── shell.go ├── shell_test.go ├── suite_test.go ├── tag.go ├── tag_test.go ├── testdata-symlink │ ├── link │ └── test │ │ └── index.html ├── testdata │ ├── .tsuru │ │ └── plugins │ │ │ ├── myplugin │ │ │ └── otherplugin.exe │ ├── archivedplugins │ │ ├── myplugin.tar.gz │ │ └── myplugin.zip │ ├── cert │ │ ├── server.crt │ │ └── server.key │ ├── deploy │ │ ├── directory │ │ │ └── file.txt │ │ ├── file1.txt │ │ └── file2.txt │ ├── deploy2 │ │ ├── .tsuruignore │ │ ├── directory │ │ │ ├── dir2 │ │ │ │ └── file.txt │ │ │ └── file.txt │ │ ├── file1.txt │ │ └── file2.txt │ ├── deploy3 │ │ ├── .gitignore │ │ └── .tsuruignore │ ├── deploy4 │ │ ├── Dockerfile │ │ └── app.sh │ └── deploy5 │ │ ├── Dockerfile │ │ └── job.sh ├── token.go ├── token_test.go ├── unit.go ├── unit_test.go ├── volume.go ├── volume_test.go ├── webhook.go └── webhook_test.go ├── config ├── diff │ ├── diff.go │ └── diff_test.go └── selfupdater │ ├── packagecloudrepo.go │ ├── packagecloudrepo_test.go │ ├── suite_test.go │ ├── uptodate.go │ └── uptodate_test.go ├── formatter ├── date.go ├── date_test.go ├── json.go ├── stream.go └── suite_test.go ├── http ├── client.go ├── client_test.go ├── transport.go └── transport_test.go ├── main.go └── main_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | bin 3 | docs 4 | misc 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [ubuntu-latest, windows-latest, macOS-latest] 9 | steps: 10 | 11 | - uses: actions/setup-go@v5 12 | with: 13 | go-version: 1.21 14 | 15 | - uses: actions/checkout@v4 16 | 17 | - uses: actions/cache@v4 18 | with: 19 | path: ~/go/pkg/mod 20 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 21 | restore-keys: | 22 | ${{ runner.os }}-go- 23 | 24 | - run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... 25 | shell: bash 26 | 27 | - name: Upload coverage to Codecov 28 | uses: codecov/codecov-action@v4 29 | if: matrix.os == 'ubuntu-latest' 30 | with: 31 | token: ${{ secrets.CODECOV_TOKEN }} 32 | 33 | lint: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/setup-go@v5 37 | with: 38 | go-version: 1.21 39 | - uses: actions/checkout@v4 40 | - run: make metalint 41 | 42 | docker_deploy: 43 | if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/') 44 | name: "publish image on dockerhub" 45 | needs: [test, lint] 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: docker/setup-qemu-action@v2 50 | - uses: docker/setup-buildx-action@v2 51 | - uses: actions/cache@v4 52 | with: 53 | path: /tmp/.buildx-cache 54 | key: ${{ runner.os }}-buildx-${{ github.sha }} 55 | restore-keys: | 56 | ${{ runner.os }}-buildx- 57 | - name: List docker image tags 58 | uses: docker/metadata-action@v4 59 | id: dockermeta 60 | with: 61 | images: tsuru/client 62 | tags: | 63 | type=match,value=latest,pattern=\d.\d.\d 64 | type=edge 65 | type=ref,event=branch 66 | type=semver,pattern={{version}} 67 | type=semver,pattern={{major}}.{{minor}} 68 | type=semver,pattern={{major}} 69 | 70 | - uses: docker/login-action@v2 71 | with: 72 | username: ${{ secrets.DOCKERHUB_USERNAME }} 73 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 74 | 75 | - name: Get version from tag 76 | id: gittag 77 | uses: jimschubert/query-tag-action@v1 78 | with: 79 | commit-ish: HEAD 80 | 81 | - name: Build and Push image to docker hub 82 | uses: docker/build-push-action@v3 83 | with: 84 | file: ./Dockerfile 85 | build-args: | 86 | TSURU_BUILD_VERSION="${{steps.gittag.outputs.tag}}" 87 | push: true 88 | tags: ${{ steps.dockermeta.outputs.tags }} 89 | labels: ${{ steps.dockermeta.outputs.labels }} 90 | cache-from: type=local,src=/tmp/.buildx-cache 91 | cache-to: type=local,dest=/tmp/.buildx-cache 92 | platforms: linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8 93 | 94 | deploy: 95 | if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/') 96 | needs: [test, lint] 97 | runs-on: ubuntu-latest 98 | 99 | permissions: 100 | contents: write 101 | 102 | steps: 103 | - uses: actions/setup-go@v5 104 | with: 105 | go-version: 1.21 106 | 107 | - uses: actions/checkout@v4 108 | with: 109 | fetch-depth: 0 110 | 111 | - name: release 112 | uses: goreleaser/goreleaser-action@v4 113 | with: 114 | version: latest 115 | args: release --clean 116 | env: 117 | # The automatic GitHub token (namely secrets.GITHUB_TOKEN) doesn't have permission cross-project. 118 | HOMEBREW_TSURU_REPOSITORY_AUTH_TOKEN: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 119 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 120 | AUR_KEY: ${{ secrets.PRIVATE_SSH_KEY }} 121 | 122 | - uses: ruby/setup-ruby@v1 123 | with: 124 | ruby-version: '3.0' 125 | 126 | - name: packagecloud 127 | run: ./misc/build_publish_to_packagecloud.sh 128 | env: 129 | PACKAGE_NAME: tsuru-client 130 | PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} 131 | SKIP_GORELEASER: "true" 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tsuru/tsuru 2 | docs/build 3 | *.pyc 4 | *.pem 5 | dist 6 | bin 7 | *.txt 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | tests: true 4 | linters: 5 | enable: 6 | - misspell 7 | disable: 8 | - errcheck 9 | exclusions: 10 | generated: lax 11 | presets: 12 | - comments 13 | - common-false-positives 14 | - legacy 15 | - std-error-handling 16 | paths: 17 | - third_party$ 18 | - builtin$ 19 | - examples$ 20 | formatters: 21 | enable: 22 | - gofmt 23 | - goimports 24 | settings: 25 | gofmt: 26 | simplify: true 27 | exclusions: 28 | generated: lax 29 | paths: 30 | - third_party$ 31 | - builtin$ 32 | - examples$ 33 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # goreleaser.yml 2 | # Build customization 3 | project_name: tsuru 4 | version: 2 5 | builds: 6 | - main: ./tsuru 7 | binary: tsuru 8 | goos: 9 | - windows 10 | - darwin 11 | - linux 12 | goarch: 13 | - amd64 14 | - arm64 15 | ignore: 16 | - goos: windows 17 | goarch: arm64 18 | env: 19 | - CGO_ENABLED=0 20 | - META_PROJECT_NAME={{.ProjectName}} 21 | - META_VERSION={{.Version}} 22 | - META_TAG={{.Tag}} 23 | - META_PREVIOUS_TAG={{.PreviousTag}} 24 | - META_COMMIT={{.Commit}} 25 | - META_DATE={{.Date}} 26 | mod_timestamp: '{{ .CommitTimestamp }}' 27 | flags: 28 | - -trimpath 29 | ldflags: 30 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} 31 | hooks: 32 | post: 33 | - ./misc/generate_metadata.sh dist/metadata.json 34 | 35 | 36 | # Archive customization 37 | archives: 38 | - name_template: >- 39 | {{ .ProjectName }}_ 40 | {{- .Version }}_ 41 | {{- if eq .Os "darwin" -}} 42 | macOS 43 | {{- else -}} 44 | {{ .Os }} 45 | {{- end }}_ 46 | {{- .Arch }} 47 | format: tar.gz 48 | format_overrides: 49 | - goos: windows 50 | format: zip 51 | files: 52 | - misc/bash-completion 53 | - misc/zsh-completion 54 | 55 | release: 56 | extra_files: 57 | - glob: dist/metadata.json 58 | - glob: dist/CHANGELOG.md 59 | 60 | # If set to auto, will mark the release as not ready for production 61 | # in case there is an indicator for this in the tag e.g. v1.0.0-rc1 62 | prerelease: auto 63 | 64 | # Mac OS Homebrew 65 | brews: 66 | # Reporitory to push the tap to. 67 | - repository: 68 | owner: tsuru 69 | name: homebrew-tsuru 70 | token: "{{ .Env.HOMEBREW_TSURU_REPOSITORY_AUTH_TOKEN }}" 71 | 72 | description: "tsuru-client is a tsuru command line tool for application developers." 73 | homepage: "https://docs.tsuru.io/stable/" 74 | 75 | # Folder inside the repository to put the formula. 76 | # Default is the root folder. 77 | directory: Formula 78 | 79 | # Custom install 80 | install: | 81 | bin.install "tsuru" 82 | bash_completion.install "misc/bash-completion" => "tsuru" 83 | zsh_completion.install "misc/zsh-completion" => "tsuru" 84 | 85 | # If set to auto, the release will not be uploaded to the homebrew tap 86 | # in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1 87 | skip_upload: auto 88 | 89 | aurs: 90 | - name: tsuru-bin 91 | description: "tsuru-client is a tsuru command line tool for application developers." 92 | homepage: "https://tsuru.io" 93 | license: "BSD-3-Clause" 94 | 95 | maintainers: 96 | - "Tsuru " 97 | - "Claudio Netto " 98 | 99 | skip_upload: auto 100 | 101 | git_url: "ssh://aur@aur.archlinux.org/tsuru-bin.git" 102 | private_key: "{{ .Env.AUR_KEY }}" 103 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of tsuru-client authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | 5 | Alessandro Corbelli 6 | Dan Carley 7 | Globo.com 8 | Lucas Weiblen 9 | Marc Abramowitz 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest! Before you write any code, please read our [contributing document][contributing], which describes our procedures and methods. 4 | 5 | [contributing]: https://docs.tsuru.io/master/contributing/index.html 6 | 7 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the official list of people who have contributed code to 2 | # tsuru-client. 3 | # The AUTHORS file lists the copyright holders; this file lists people. 4 | # For example, Globo.com employees are listed here but not in AUTHORS, 5 | # because Globo.com holds the copyright. 6 | # 7 | # You can update this list using the following command: 8 | # 9 | # % git shortlog -se | awk '{$1=""; print $0}' | sed -e 's/^ //' 10 | # 11 | # Please keep this file sorted, and group users with multiple emails. 12 | 13 | Alessandro Corbelli 14 | Andrews Medina 15 | Cezar Sa Espinola 16 | Claudio Netto 17 | Dan Carley 18 | Denis Aoki 19 | Diego Fleury 20 | Diogo Munaro Vieira 21 | Flavia Missi 22 | Francisco Souza 23 | Guilherme Garnier 24 | Gustavo Pantuza Coelho Pinto 25 | Joao Paulo Vieira 26 | Jonathan Prates 27 | Lucas Weiblen 28 | Marc Abramowitz 29 | Paulo Sousa 30 | Raul Freitas 31 | Rodrigo Machado 32 | Tarsis Azevedo 33 | Wilson Júnior 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-alpine AS builder 2 | 3 | RUN apk add --update --no-cache \ 4 | gcc \ 5 | git \ 6 | make \ 7 | musl-dev \ 8 | && : 9 | 10 | WORKDIR /go/src/github.com/tsuru/tsuru-client 11 | COPY . /go/src/github.com/tsuru/tsuru-client 12 | 13 | ARG TSURU_BUILD_VERSION 14 | RUN make build && echo 1 15 | 16 | FROM alpine:3.9 17 | 18 | RUN apk update && \ 19 | apk add --no-cache ca-certificates && \ 20 | rm /var/cache/apk/* 21 | 22 | COPY --from=builder /go/src/github.com/tsuru/tsuru-client/bin/tsuru /bin/tsuru 23 | 24 | CMD ["tsuru"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, tsuru-client authors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Globo.com nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 tsuru-client authors. All rights reserved. 2 | # Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file. 4 | 5 | # Python interpreter path 6 | PYTHON := $(shell which python) 7 | 8 | GHR := $(shell which ghr) 9 | GITHUB_TOKEN := $(shell git config --global --get github.token || echo $$GITHUB_TOKEN) 10 | GIT_TAG_VER := $(shell git describe --tags 2>/dev/null || echo "$${TSURU_BUILD_VERSION:-dev}") 11 | 12 | release: 13 | @if [ ! $(version) ]; then \ 14 | echo "version parameter is required... use: make release version="; \ 15 | exit 1; \ 16 | fi 17 | @if [ "$(GHR)" == "" ]; then \ 18 | echo "ghr is required. Instructions: github.com/tcnksm/ghr"; \ 19 | exit 1; \ 20 | fi 21 | @if [ ! "$(GITHUB_TOKEN)" ]; then \ 22 | echo "github token should be configurated. Instructions: github.com/tcnksm/ghr"; \ 23 | exit 1; \ 24 | fi 25 | 26 | @echo " ==> Releasing tsuru $(version) version." 27 | 28 | @echo " ==> Building binaries." 29 | @./misc/build-all.sh 30 | 31 | @echo " ==> Bumping version." 32 | @git add tsuru/main.go 33 | @git commit -m "bump to $(version)" 34 | 35 | @echo " ==> Creating tag." 36 | 37 | @git tag $(version) 38 | 39 | @echo " ==> Uploading binaries to github." 40 | 41 | ghr --repository tsuru-client --username tsuru --draft --recreate $(version) dist/ 42 | 43 | @echo " ==> Pushing changes to github." 44 | 45 | @git push --tags 46 | @git push origin main 47 | 48 | doc-requirements: install 49 | @pip install -r requirements.txt 50 | 51 | docs-clean: 52 | @rm -rf ./docs/build 53 | 54 | doc: docs-clean doc-requirements 55 | @tsuru_sphinx tsuru docs/ && cd docs && make html SPHINXOPTS="-N -W" 56 | 57 | docs: doc 58 | 59 | docker-test: 60 | docker run --rm -v ${PWD}:/go/src/github.com/tsuru/tsuru-client -w /go/src/github.com/tsuru/tsuru-client golang:latest sh -c "make test" 61 | 62 | test: 63 | go test -race ./... -check.v 64 | 65 | install: 66 | go install ./... 67 | 68 | build-all: 69 | ./misc/build-all.sh 70 | 71 | build: 72 | go build -ldflags "-s -w -X 'main.version=$(GIT_TAG_VER)'" -o ./bin/tsuru ./tsuru 73 | 74 | check-docs: build 75 | ./misc/check-all-cmds-docs.sh 76 | 77 | godownloader: 78 | git clone https://github.com/goreleaser/godownloader.git /tmp/godownloader 79 | cd /tmp/godownloader && go install . 80 | rm -rf /tmp/godownloader 81 | 82 | install.sh: .goreleaser.yml godownloader 83 | godownloader --repo tsuru/tsuru-client $< >$@ 84 | 85 | install-scripts: install.sh 86 | 87 | metalint: 88 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin 89 | go install ./... 90 | $$(go env GOPATH)/bin/golangci-lint run -c ./.golangci.yml ./... 91 | 92 | .PHONY: doc docs release manpage godownloader install-scripts 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tsuru-client 2 | 3 | [![Actions Status](https://github.com/tsuru/tsuru-client/workflows/Go/badge.svg)](https://github.com/tsuru/tsuru-client/actions) 4 | [![codecov](https://codecov.io/gh/tsuru/tsuru-client/branch/master/graph/badge.svg)](https://codecov.io/gh/tsuru/tsuru-client) 5 | 6 | tsuru is a command line for application developers on 7 | [tsuru](https://github.com/tsuru/tsuru). 8 | 9 | ## reporting issues 10 | 11 | Please report issues to the 12 | [tsuru/tsuru](https://github.com/tsuru/tsuru/issues) repository. 13 | 14 | 15 | ## Environment variables 16 | 17 | The following environment variables can be used to configure the client: 18 | 19 | ### API configuration 20 | 21 | * `TSURU_TARGET`: the tsuru API endpoint. 22 | * `TSURU_TOKEN`: the tsuru API token. 23 | 24 | ### Other configuration 25 | 26 | * `TSURU_CLIENT_FORCE_CHECK_UPDATES`: boolean on whether to force checking for 27 | updates. When `true`, it hangs if no response from remote server! (default: unset) 28 | * `TSURU_CLIENT_LOCAL_TIMEOUT`: timeout for performing local non-critical operations 29 | (eg: writing preferences to `~/.tsuru/config.json`). (default: 1s) 30 | * `TSURU_CLIENT_SELF_UPDATE_SNOOZE_DURATION`: snooze the self-updating process for 31 | the given duration. (default: 0s) 32 | 33 | ## Tsuru plugins 34 | 35 | Tsuru plugins are the standard way to extend tsuru-client functionality transparently. 36 | Installing and using a plugin is done with: 37 | ``` 38 | tsuru plugin install 39 | tsuru 40 | ``` 41 | 42 | For developing a custom plugin, read about [Developing Tsuru Plugins](./plugins.md). 43 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Copyright 2015 tsuru-client authors. All rights reserved. 2 | Use of this source code is governed by a BSD-style 3 | license that can be found in the LICENSE file. 4 | 5 | **tsuru** is the command line utility used by application developers, that 6 | will allow users to create, list, bind and manage apps. 7 | 8 | .. note:: 9 | 10 | This documentation is a reference of **tsuru** command line interface. 11 | If you want know about how to use tsuru, you should see the `tsuru documentation `_. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | installing 17 | reference 18 | -------------------------------------------------------------------------------- /docs/source/installing.rst: -------------------------------------------------------------------------------- 1 | .. Copyright 2015 tsuru-client authors. All rights reserved. 2 | Use of this source code is governed by a BSD-style 3 | license that can be found in the LICENSE file. 4 | 5 | Installing 6 | ========== 7 | 8 | There are several ways to install `tsuru-client`: 9 | 10 | - `Downloading binaries (Mac OS X and Linux)`_ 11 | - `Using homebrew (Mac OS X only)`_ 12 | - `Using the PPA (Ubuntu only)`_ 13 | - `Using AUR (ArchLinux only)`_ 14 | - `Build from source (Linux and Mac OS X)`_ 15 | 16 | Downloading binaries (Mac OS X and Linux) 17 | ----------------------------------------- 18 | 19 | We provide pre-built binaries for OS X and Linux, only for the amd64 20 | architecture. You can download these binaries directly from the releases page 21 | of the project: 22 | 23 | * tsuru: https://github.com/tsuru/tsuru-client/releases 24 | 25 | Using homebrew (Mac OS X only) 26 | ------------------------------ 27 | 28 | If you use Mac OS X and `homebrew `_, you may 29 | use a custom tap to install ``tsuru``. First you need to add the tap: 30 | 31 | .. highlight:: bash 32 | 33 | :: 34 | 35 | $ brew tap tsuru/homebrew-tsuru 36 | 37 | Now you can install tsuru: 38 | 39 | .. highlight:: bash 40 | 41 | :: 42 | 43 | $ brew install tsuru 44 | 45 | Whenever a new version of any of tsuru's clients is out, you can just run: 46 | 47 | .. highlight:: bash 48 | 49 | :: 50 | 51 | $ brew update 52 | $ brew upgrade tsuru 53 | 54 | For more details on taps, check `homebrew documentation 55 | `_. 56 | 57 | **NOTE:** tsuru requires Go 1.2 or higher. Make sure you have the last version 58 | of Go installed in your system. 59 | 60 | Using the PPA (Ubuntu only) 61 | --------------------------- 62 | 63 | Ubuntu users can install tsuru clients using ``apt-get`` and the `tsuru PPA 64 | `_. You'll need to add the PPA 65 | repository locally and run an ``apt-get update``: 66 | 67 | .. highlight:: bash 68 | 69 | :: 70 | 71 | $ sudo apt-add-repository ppa:tsuru/ppa 72 | $ sudo apt-get update 73 | 74 | Now you can install tsuru's clients: 75 | 76 | .. highlight:: bash 77 | 78 | :: 79 | 80 | $ sudo apt-get install tsuru-client 81 | 82 | Using AUR (ArchLinux only) 83 | -------------------------- 84 | 85 | Archlinux users can build and install tsuru client from AUR repository, 86 | Is needed to have installed `yaourt `_ program. 87 | 88 | You can run: 89 | 90 | 91 | .. highlight:: bash 92 | 93 | :: 94 | 95 | $ yaourt -S tsuru 96 | 97 | Build from source (Linux and Mac OS X) 98 | -------------------------------------- 99 | 100 | .. note:: 101 | 102 | If you're feeling adventurous, you can try it on other systems, like 103 | FreeBSD, OpenBSD or even Windows. Please let us know about your progress! 104 | 105 | `tsuru client source `_ is written in `Go 106 | `_, so before installing tsuru from source, please make sure 107 | you have `installed and configured Go `_. 108 | 109 | With Go installed and configured, you can use ``go get`` to install any of 110 | tsuru's clients: 111 | 112 | .. highlight:: bash 113 | 114 | :: 115 | 116 | $ go get github.com/tsuru/tsuru-client/tsuru 117 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tsuru/tsuru-client 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/Masterminds/semver/v3 v3.1.1 9 | github.com/antihax/optional v1.0.0 10 | github.com/cezarsa/form v0.0.0-20210510165411-863b166467b9 11 | github.com/ghodss/yaml v1.0.0 12 | github.com/hashicorp/go-version v1.2.0 13 | github.com/lnquy/cron v1.1.1 14 | github.com/mattn/go-shellwords v1.0.12 15 | github.com/mitchellh/go-wordwrap v1.0.1 16 | github.com/pkg/errors v0.9.1 17 | github.com/pmorie/go-open-service-broker-client v0.0.0-20180330214919-dca737037ce6 18 | github.com/sabhiram/go-gitignore v0.0.0-20171017070213-362f9845770f 19 | github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa 20 | github.com/tsuru/go-tsuruclient v0.0.0-20241114131333-e45b7f4741d8 21 | github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6 22 | github.com/tsuru/tsuru v0.0.0-20250310175958-77c4d1d51a56 23 | golang.org/x/net v0.36.0 24 | golang.org/x/oauth2 v0.20.0 25 | golang.org/x/sys v0.30.0 26 | golang.org/x/term v0.29.0 27 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c 28 | gopkg.in/yaml.v2 v2.4.0 29 | k8s.io/apimachinery v0.26.2 30 | k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 31 | ) 32 | 33 | require ( 34 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 35 | github.com/beorn7/perks v1.0.1 // indirect 36 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625 // indirect 37 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 38 | github.com/containerd/containerd v1.6.38 // indirect 39 | github.com/containerd/log v0.1.0 // indirect 40 | github.com/docker/cli v23.0.0-rc.1+incompatible // indirect 41 | github.com/docker/docker v25.0.6+incompatible // indirect 42 | github.com/docker/docker-credential-helpers v0.7.0 // indirect 43 | github.com/docker/go-units v0.5.0 // indirect 44 | github.com/globocom/mongo-go-prometheus v0.1.1 // indirect 45 | github.com/go-logr/logr v1.3.0 // indirect 46 | github.com/gogo/protobuf v1.3.2 // indirect 47 | github.com/golang/glog v1.2.4 // indirect 48 | github.com/golang/protobuf v1.5.4 // indirect 49 | github.com/golang/snappy v0.0.4 // indirect 50 | github.com/google/gofuzz v1.2.0 // indirect 51 | github.com/howeyc/fsnotify v0.9.0 // indirect 52 | github.com/json-iterator/go v1.1.12 // indirect 53 | github.com/klauspost/compress v1.16.0 // indirect 54 | github.com/kr/pretty v0.3.0 // indirect 55 | github.com/kr/text v0.2.0 // indirect 56 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 57 | github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c // indirect 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 59 | github.com/modern-go/reflect2 v1.0.2 // indirect 60 | github.com/montanaflynn/stats v0.7.0 // indirect 61 | github.com/morikuni/aec v1.0.0 // indirect 62 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect 63 | github.com/opentracing-contrib/go-stdlib v1.0.1-0.20201028152118-adbfc141dfc2 // indirect 64 | github.com/opentracing/opentracing-go v1.2.0 // indirect 65 | github.com/prometheus/client_golang v1.14.0 // indirect 66 | github.com/prometheus/client_model v0.3.0 // indirect 67 | github.com/prometheus/common v0.42.0 // indirect 68 | github.com/prometheus/procfs v0.9.0 // indirect 69 | github.com/robfig/cron/v3 v3.0.1 // indirect 70 | github.com/rogpeppe/go-internal v1.6.1 // indirect 71 | github.com/sajari/fuzzy v1.0.0 // indirect 72 | github.com/sirupsen/logrus v1.9.3 // indirect 73 | github.com/tsuru/config v0.0.0-20201023175036-375aaee8b560 // indirect 74 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 75 | github.com/xdg-go/scram v1.1.2 // indirect 76 | github.com/xdg-go/stringprep v1.0.4 // indirect 77 | github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect 78 | go.mongodb.org/mongo-driver v1.15.0 // indirect 79 | golang.org/x/crypto v0.35.0 // indirect 80 | golang.org/x/sync v0.11.0 // indirect 81 | golang.org/x/text v0.22.0 // indirect 82 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect 83 | google.golang.org/grpc v1.65.0 // indirect 84 | google.golang.org/protobuf v1.34.1 // indirect 85 | gopkg.in/inf.v0 v0.9.1 // indirect 86 | k8s.io/api v0.26.2 // indirect 87 | k8s.io/client-go v0.26.2 // indirect 88 | k8s.io/klog/v2 v2.90.1 // indirect 89 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 90 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 91 | sigs.k8s.io/yaml v1.3.0 // indirect 92 | ) 93 | -------------------------------------------------------------------------------- /misc/bash-completion: -------------------------------------------------------------------------------- 1 | # Copyright 2015 tsuru-client authors. All rights reserved. 2 | # Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file. 4 | 5 | _tsuru() { 6 | local tasks=`tsuru | egrep -o "^ [^A-Z]*([A-Z]|$)" | sed -e 's/^[[:space:]]*//' | sed -e 's/[[:space:]A-Z]*$//' | sed 's/ /-/g'` 7 | 8 | let last_complete=COMP_CWORD-1 9 | 10 | # TODO(cezarsa): Parse flags from help is possible 11 | local main_flags_with_args=("-t" "--target" "-v" "--verbosity") 12 | local base_cmd="" 13 | local ignore_arg=0 14 | for i in $(seq 1 1 $last_complete 2>/dev/null); do 15 | local current=${COMP_WORDS[i]} 16 | if [[ "${current}" == "=" ]]; then 17 | continue 18 | fi 19 | if [[ $ignore_arg == 1 ]]; then 20 | ignore_arg=0 21 | continue 22 | fi 23 | if [[ "${current}" == "-"* ]]; then 24 | if [[ $i != 1 ]]; then 25 | continue 26 | fi 27 | for flag in ${main_flags_with_args[@]}; do 28 | if [[ "${current}" == "${flag}" ]]; then 29 | ignore_arg=1 30 | fi 31 | done 32 | continue 33 | fi 34 | base_cmd="${base_cmd}${current}-" 35 | done 36 | local incomplete_command="${base_cmd}${COMP_WORDS[COMP_CWORD]}" 37 | local genlist=$(compgen -W "$tasks" -- "$incomplete_command") 38 | genlist=$(echo "$genlist" | sed "s/^${base_cmd}//" | sed 's/-.*$//') 39 | COMPREPLY=( $(compgen -W "$genlist") ) 40 | } 41 | complete -F _tsuru -o bashdefault -o default tsuru 42 | -------------------------------------------------------------------------------- /misc/build-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NAME="tsuru" 4 | VERSION=$1 5 | if [ "$VERSION" == "" ] 6 | then 7 | VERSION=$(cat tsuru/main.go | grep "version =" | cut -d '"' -f2) 8 | fi 9 | OSSES="darwin linux windows" 10 | ARCHS="amd64 386" 11 | 12 | mkdir dist || true 13 | 14 | for os in $OSSES; do 15 | for arch in $ARCHS; do 16 | if [[ $os == "darwin" ]] && [[ $arch == "386" ]]; then 17 | continue 18 | fi 19 | echo "Building version $VERSION for $os $arch" 20 | dest="dist/${NAME}" 21 | zipname="dist/${NAME}-${VERSION}-${os}_${arch}" 22 | GOOS=$os GOARCH=$arch go build -o $dest ./tsuru 23 | if [[ $os == "windows" ]]; then 24 | mv $dest "${dest}.exe" 25 | zip "${zipname}.zip" "${dest}.exe" 26 | tar -zcpvf "${zipname}.tar.gz" "${dest}.exe" 27 | rm "${dest}.exe" 28 | else 29 | tar -zcpvf "${zipname}.tar.gz" $dest 30 | rm $dest 31 | fi 32 | done 33 | done 34 | -------------------------------------------------------------------------------- /misc/build_publish_to_packagecloud.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | PACKAGE_NAME="tsuru-client" 5 | PACKAGE_VERSION="${GITHUB_REF#"refs/tags/"}" 6 | [ "${GITHUB_REF}" == "" ] && echo "No GITHUB_REF found, exiting" && exit 1 7 | [ "${PACKAGE_VERSION}" == "" ] && echo "PACKAGE_VERSION is empty, exiting" && exit 1 8 | [ "${PACKAGECLOUD_TOKEN}" == "" ] && echo "No packagecloud token found, exiting" && exit 1 9 | 10 | PACKAGECLOUD_REPO="tsuru/rc" 11 | if [[ ${PACKAGE_VERSION} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 12 | PACKAGECLOUD_REPO="tsuru/stable" 13 | fi 14 | __soft_errors=0 15 | __soft_errors_str="" 16 | 17 | _install_dependencies() { 18 | if ! command -v rpm &>/dev/null ; then 19 | sudo apt-get update 20 | sudo apt-get install -y rpm 21 | fi 22 | if ! gem list -i fpm -v ">= 1.14.2" &>/dev/null ; then 23 | gem install fpm -v '>= 1.14.2' 24 | fi 25 | if ! gem list -i package_cloud -v ">= 0.3.10" &>/dev/null ; then 26 | gem install package_cloud -v '>= 0.3.10' 27 | fi 28 | } 29 | 30 | _build_package() { 31 | type="$1" ; shift 32 | package_name="$1" ; shift 33 | version="$1" ; shift 34 | input_dir="$1" ; shift 35 | arch=$1 ; shift 36 | output_file="$1" ; shift 37 | 38 | description="tsuru is the command line interface for the tsuru server 39 | 40 | Tsuru is an open source platform as a service software. This package installs 41 | the client used by application developers to communicate with tsuru server." 42 | 43 | [ "${arch}" = "386" ] && arch="i386" 44 | 45 | fpm \ 46 | -s dir -t "${type}" \ 47 | -p "${output_file}" \ 48 | --chdir "${input_dir}" \ 49 | --name "${package_name}" \ 50 | --version "${version}" \ 51 | --architecture "${arch}" \ 52 | --maintainer "tsuru@g.globo" \ 53 | --vendor "Tsuru team " \ 54 | --url "https://tsuru.io" \ 55 | --description "${description}" \ 56 | --no-depends \ 57 | --license bsd3 \ 58 | --log INFO \ 59 | tsuru=/usr/bin/tsuru 60 | } 61 | 62 | _build_all_packages(){ 63 | INPUT_DIRS=$(find dist -type d -name "tsuru_linux_*") 64 | for INPUT_DIR in $INPUT_DIRS; do 65 | ARCH=$(echo "${INPUT_DIR}" | sed -e 's/.*tsuru_linux_\([^_]\+\)_.*/\1/' ) 66 | PACKAGE_FILE_DEB="${PACKAGE_NAME}_${PACKAGE_VERSION}_${ARCH}.deb" 67 | PACKAGE_FILE_RPM="${PACKAGE_NAME}_${PACKAGE_VERSION}_${ARCH}.rpm" 68 | 69 | set +e 70 | echo "Building .deb ${PACKAGE_FILE_DEB}..." 71 | if ! _build_package "deb" "${PACKAGE_NAME}" "${PACKAGE_VERSION}" "${INPUT_DIR}" "${ARCH}" "dist/${PACKAGE_FILE_DEB}" ; then 72 | _=$(( __soft_errors++ )) 73 | __soft_errors_str="${__soft_errors_str}\nFailed to build ${PACKAGE_FILE_DEB}." 74 | fi 75 | echo "Building .rpm ${PACKAGE_FILE_RPM}..." 76 | if ! _build_package "rpm" "${PACKAGE_NAME}" "${PACKAGE_VERSION}" "${INPUT_DIR}" "${ARCH}" "dist/${PACKAGE_FILE_RPM}" ; then 77 | _=$(( __soft_errors++ )) 78 | __soft_errors_str="${__soft_errors_str}\nFailed to build ${PACKAGE_FILE_DEB}." 79 | fi 80 | set -e 81 | done 82 | } 83 | 84 | _publish_all_packages(){ 85 | DEB_DISTROS=" 86 | any/any 87 | debian/jessie 88 | debian/stretch 89 | debian/buster 90 | debian/bullseye 91 | 92 | linuxmint/sarah 93 | linuxmint/serena 94 | linuxmint/sonya 95 | linuxmint/sylvia 96 | linuxmint/tara 97 | linuxmint/tessa 98 | linuxmint/tina 99 | linuxmint/tricia 100 | linuxmint/ulyana 101 | 102 | ubuntu/bionic 103 | ubuntu/focal 104 | ubuntu/jammy 105 | ubuntu/trusty 106 | ubuntu/xenial 107 | ubuntu/zesty 108 | " 109 | while read -r PACKAGE_FILE; do 110 | for DEB_DISTRO in $DEB_DISTROS; do 111 | [ "${DEB_DISTRO}" = "" ] && continue 112 | 113 | # XXX: Getting ready for supporting any/any publishing. Publish non-amd64 for any/any only 114 | [[ ! "${PACKAGE_FILE}" =~ "amd64" ]] && [ "${DEB_DISTRO}" != "any/any" ] && continue 115 | 116 | echo "Pushing ${PACKAGE_FILE} to packagecloud (${DEB_DISTRO})..." 117 | set +e 118 | if ! package_cloud push "${PACKAGECLOUD_REPO}/${DEB_DISTRO}" "${PACKAGE_FILE}" ; then 119 | _=$(( __soft_errors++ )) 120 | __soft_errors_str="${__soft_errors_str}\nFailed to publish ${PACKAGE_FILE} (${DEB_DISTRO})." 121 | fi 122 | set -e 123 | done 124 | done < <(find dist -type f -name "*.deb") 125 | 126 | RPM_DISTROS=" 127 | rpm_any/rpm_any 128 | el/6 129 | el/7 130 | 131 | fedora/31 132 | fedora/32 133 | fedora/33 134 | " 135 | while read -r PACKAGE_FILE; do 136 | for RPM_DISTRO in $RPM_DISTROS; do 137 | [ "${RPM_DISTRO}" = "" ] && continue 138 | echo "Pushing ${PACKAGE_FILE} to packagecloud..." 139 | set +e 140 | if ! package_cloud push "${PACKAGECLOUD_REPO}/${RPM_DISTRO}" "${PACKAGE_FILE}" ; then 141 | _=$(( __soft_errors++ )) 142 | __soft_errors_str="${__soft_errors_str}\nFailed to publish ${PACKAGE_FILE} (${RPM_DISTRO})." 143 | fi 144 | set -e 145 | done 146 | done < <(find dist -type f -name "*.rpm") 147 | } 148 | 149 | ## Main 150 | 151 | _install_dependencies 152 | _build_all_packages 153 | _publish_all_packages 154 | 155 | if [ "${__soft_errors}" != "0" ] ; then 156 | echo "We got ${__soft_errors} (soft) errors." 157 | echo -e "${__soft_errors_str}" 158 | exit 1 159 | fi 160 | -------------------------------------------------------------------------------- /misc/check-all-cmds-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 tsuru authors. All rights reserved. 4 | # Use of this source code is governed by a BSD-style 5 | # license that can be found in the LICENSE file. 6 | 7 | for c in `./bin/tsuru | grep "Available commands" -A 500 | cut -f3 -d' ' | sort -u` 8 | do 9 | cat ./docs/source/reference.rst | grep "$c" >/dev/null 2>&1 10 | RESULT=$? 11 | if [ $RESULT -eq 1 ] 12 | then 13 | echo "${c} is not documented" 14 | fi 15 | done 16 | -------------------------------------------------------------------------------- /misc/check-license.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Copyright 2016 tsuru authors. All rights reserved. 4 | # Use of this source code is governed by a BSD-style 5 | # license that can be found in the LICENSE file. 6 | 7 | status=0 8 | for f in `git ls-files | xargs grep -L "Copyright" | grep ".go" | grep -v vendor/` 9 | do 10 | echo $f 11 | status=1 12 | done 13 | 14 | if [ $status != 0 ] 15 | then 16 | exit $status 17 | fi 18 | 19 | for f in `git ls-files | xargs grep "Copyright 201[2345]" -l | grep -v check-license.sh | grep -v vendor/` 20 | do 21 | date=`git log -1 --format="%ad" --date=short -- $f` 22 | if [ `echo "$date" | grep ^2016` ] 23 | then 24 | echo $f $date 25 | status=1 26 | fi 27 | done 28 | 29 | exit $status 30 | -------------------------------------------------------------------------------- /misc/generate_metadata.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright © 2023 tsuru-client authors 3 | # Use of this source code is governed by a BSD-style 4 | # license that can be found in the LICENSE file. 5 | 6 | set -eu 7 | output_file="${1:-}" 8 | [ -z "$output_file" ] && echo "Usage: $0 " && exit 1 9 | 10 | fail=0 11 | [ -z "${META_PROJECT_NAME:-}" ] && echo "No META_PROJECT_NAME env" && _=$(( fail+=1 )) 12 | [ -z "${META_VERSION:-}" ] && echo "No META_VERSION env" && _=$(( fail+=1 )) 13 | [ -z "${META_TAG:-}" ] && echo "No META_TAG env" && _=$(( fail+=1 )) 14 | # [ -z "${META_PREVIOUS_TAG:-}" ] && echo "No META_PREVIOUS_TAG env" && _=$(( fail+=1 )) 15 | [ -z "${META_COMMIT:-}" ] && echo "No META_COMMIT env" && _=$(( fail+=1 )) 16 | [ -z "${META_DATE:-}" ] && echo "No META_DATE env" && _=$(( fail+=1 )) 17 | [ "$fail" -gt 0 ] && exit 1 18 | 19 | echo "Generating metadata for ${output_file}" 20 | cat > "$output_file" </dev/null ; then 22 | echo "installing tsuru with brew..." 23 | brew tap tsuru/homebrew-tsuru 24 | brew install tsuru 25 | exit $? 26 | fi 27 | 28 | package_type="null" 29 | if command -v "apt" >/dev/null ; then 30 | package_type="deb" 31 | package_installer="apt" 32 | elif command -v "apt-get" >/dev/null ; then 33 | package_type="deb" 34 | package_installer="apt-get" 35 | elif command -v "yum" >/dev/null ; then 36 | package_type="rpm" 37 | package_installer="yum" 38 | elif command -v "zypper" >/dev/null ; then 39 | package_type="rpm" 40 | package_installer="zypper" 41 | fi 42 | 43 | if [ "${package_type}" != "null" ]; then 44 | if command -v "curl" >/dev/null ; then 45 | echo "installing tsuru with packagecloud.io deb script (will ask for sudo password)" 46 | curl -s "https://packagecloud.io/install/repositories/tsuru/stable/script.${package_type}.sh" | sudo bash 47 | elif command -v "wget" >/dev/null ; then 48 | echo "installing tsuru with packagecloud.io rpm script (will ask for sudo password)" 49 | wget -q -O - "https://packagecloud.io/install/repositories/tsuru/stable/script.${package_type}.sh" | sudo bash 50 | else 51 | echo "curl or wget is required to install tsuru" 52 | exit 1 53 | fi 54 | 55 | # update repo config for using any/any instead of os/dist 56 | sedpattern='s@^(deb(-src)?.* https://packagecloud\.io/tsuru/stable/)\w+/?\s+\w+(.*)@\1any/ any\3@g' 57 | cfile="/etc/apt/sources.list.d/tsuru_stable.list" 58 | [ -f "${cfile}" ] && { sed -E "${sedpattern}" "${cfile}" | sudo tee "${cfile}" ; } &>/dev/null 59 | sedpattern='s@^(baseurl=https://packagecloud\.io/tsuru/stable)/\w+/\w+/(.*)@\1/any/any/\2@g' 60 | cfile="/etc/zypp/repos.d/tsuru_stable.repo" 61 | [ -f "${cfile}" ] && { sed -E "${sedpattern}" "${cfile}" | sudo tee "${cfile}" ; } &>/dev/null 62 | cfile="/etc/yum.repos.d/tsuru_stable.repo" 63 | [ -f "${cfile}" ] && { sed -E "${sedpattern}" "${cfile}" | sudo tee "${cfile}" ; } &>/dev/null 64 | 65 | # update cache 66 | echo "Updating package cache..." 67 | if command -v "apt" >/dev/null ; then 68 | sudo apt -qq update 69 | elif command -v "apt-get" >/dev/null ; then 70 | sudo apt-get -qq update 71 | elif command -v "yum" >/dev/null ; then 72 | sudo yum -q makecache -y --disablerepo='*' --enablerepo='tsuru_stable' 73 | sudo yum -q makecache -y --disablerepo='*' --enablerepo='tsuru_stable-source' 74 | elif command -v "zypper" >/dev/null ; then 75 | sudo zypper --gpg-auto-import-keys refresh tsuru_stable 76 | sudo zypper --gpg-auto-import-keys refresh tsuru_stable-source 77 | fi 78 | 79 | echo "Installing tsuru-client" 80 | sudo "${package_installer}" install tsuru-client 81 | else 82 | echo "Could not install tsuru on your OS, please download the binary from:" 83 | echo " https://github.com/tsuru/tsuru-client/releases/latest" 84 | exit 2 85 | fi 86 | -------------------------------------------------------------------------------- /misc/zsh-completion: -------------------------------------------------------------------------------- 1 | #compdef tsuru 2 | #autoload 3 | # Copyright 2017 tsuru authors. All rights reserved. 4 | # Use of this source code is governed by a BSD-style 5 | # license that can be found in the LICENSE file. 6 | 7 | _tsuru_get_commands() { 8 | local -a commands 9 | local tasks 10 | 11 | tasks=$(tsuru 2> /dev/null | awk -F ' ' 'match($0, /^[ ]/) { if (!match($0, /( help |This command was removed)/)) { printf "%s:",$1; for(i=2; i<=NF; i++) { printf "%s ",$i }; printf "\n" } }') 12 | commands=("${(f)tasks}") 13 | 14 | _describe 'Tsuru commands' commands 15 | } 16 | 17 | _tsuru() { 18 | _arguments \ 19 | "1: :_tsuru_get_commands" 20 | } 21 | 22 | _tsuru "$@" 23 | -------------------------------------------------------------------------------- /plugins.md: -------------------------------------------------------------------------------- 1 | # Developing Tsuru Plugins 2 | 3 | Tsuru plugins are the standard way to extend tsuru-client functionality transparently. 4 | 5 | A tsuru-client plugin is any runnable file, located inside `~/.tsuru/plugins` directory. 6 | It works by finding the runnable file (with or without extension) with that plugin name. 7 | 8 | A simple working example: 9 | ```bash 10 | cat > ~/.tsuru/plugins/myplugin.sh <<"EOF" 11 | #!/bin/sh 12 | echo "Hello from tsuru plugin ${TSURU_PLUGIN_NAME}!" 13 | echo " called with args: $@" 14 | EOF 15 | 16 | chmod +x ~/.tsuru/plugins/myplugin.sh 17 | 18 | tsuru myplugin subcommands -flags 19 | 20 | ##### printed: 21 | # Hello from tsuru plugin myplugin! 22 | # called with args: subcommands -flags 23 | ``` 24 | 25 | You may find available tsuru plugins on github, by searching for the topic [`tsuru-plugin`](https://github.com/topics/tsuru-plugin). 26 | (If you are developing a plugin, please tag your github repo). 27 | 28 | ## Distributing a tsuru plugin 29 | 30 | The best way to distribute a tsuru plugin is making it compatible with `tsuru plugin install`. 31 | There are different approaches for distributing the plugin, 32 | depending on the language used for building it. 33 | 34 | ### script-like single file 35 | If the plugin is bundled as a **script-like single file** (eg: shell script, python, ruby, etc...) 36 | you may make it available for download on a public URL. 37 | The name of the file is irrelevant on this case. 38 | 39 | ### bundle of multiple files 40 | If the plugin is bundled as **multiple files**, you should compact them inside a `.tar.gz` or `.zip` file, 41 | and make it available for download on a public URL. 42 | In this case, the file entrypoint must has the same name as the plugin (file extension is optional). 43 | The CLI will call the binary at `~/.tsuru/plugins/myplugin/myplugin[.ext]`. 44 | 45 | ### compiled binary 46 | If the plugin is bundled as a **compiled binary**, you should create a `manifest.json` file 47 | (as defined on issue [#172](https://github.com/tsuru/tsuru-client/issues/172)) 48 | which tells where to download the appropriate binary: 49 | ```json 50 | { 51 | "SchemaVersion": "1.0", 52 | "Metadata": { 53 | "Name": "", 54 | "Version": "" 55 | }, 56 | "UrlPerPlatform": { 57 | "/": "", 58 | ... 59 | } 60 | } 61 | ``` 62 | 63 | Each supported os/arch (check the latest release), should be compacted as `.tar.gz` or `.zip` file. 64 | All files (`manifest.json` and all compacted binaries) must available for download on public URLs. 65 | 66 | An example of such a plugin, hosted on github, is the 67 | [`rpaasv2` plugin](https://github.com/tsuru/rpaas-operator/issues/124), 68 | installed using this [manifest.json](https://github.com/tsuru/rpaas-operator/releases/latest/download/manifest.json). 69 | 70 | ## Available ENV variables 71 | 72 | When a plugin is called, the main tsuru-client passes some additional environment variables: 73 | 74 | | env | description | 75 | | ----------------- | --------------------------------------- | 76 | | TSURU_TARGET | tsuru server url (eg: https://tsuru.io) | 77 | | TSURU_TOKEN | tsuru authentication token | 78 | | TSURU_PLUGIN_NAME | name called from the main tsuru-client | 79 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==1.7.4 2 | sphinx-rtd-theme==0.3.1 3 | tsuru-sphinx==0.1.5 -------------------------------------------------------------------------------- /tsuru/admin/app.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package admin 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | 11 | "github.com/tsuru/go-tsuruclient/pkg/config" 12 | "github.com/tsuru/tsuru-client/tsuru/app" 13 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 14 | "github.com/tsuru/tsuru/cmd" 15 | ) 16 | 17 | type AppRoutesRebuild struct { 18 | app.AppNameMixIn 19 | } 20 | 21 | func (c *AppRoutesRebuild) Info() *cmd.Info { 22 | return &cmd.Info{ 23 | Name: "app-routes-rebuild", 24 | MinArgs: 0, 25 | Usage: "app-routes-rebuild ", 26 | Desc: `Rebuild routes for an application. 27 | This can be used to recover from some failure in the router that caused 28 | existing routes to be lost.`, 29 | } 30 | } 31 | 32 | func (c *AppRoutesRebuild) Run(ctx *cmd.Context) error { 33 | appName, err := c.AppNameByArgsAndFlag(ctx.Args) 34 | if err != nil { 35 | return err 36 | } 37 | url, err := config.GetURL("/apps/" + appName + "/routes") 38 | if err != nil { 39 | return err 40 | } 41 | request, err := http.NewRequest("POST", url, nil) 42 | if err != nil { 43 | return err 44 | } 45 | rsp, err := tsuruHTTP.AuthenticatedClient.Do(request) 46 | if err != nil { 47 | return err 48 | } 49 | defer rsp.Body.Close() 50 | 51 | if rsp.StatusCode == http.StatusOK { 52 | fmt.Fprintln(ctx.Stdout, "routes was rebuilt successfully") 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /tsuru/admin/app_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package admin 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | "strings" 11 | 12 | "github.com/tsuru/tsuru/cmd" 13 | "github.com/tsuru/tsuru/cmd/cmdtest" 14 | "gopkg.in/check.v1" 15 | ) 16 | 17 | func (s *S) TestAppRoutesRebuildInfo(c *check.C) { 18 | c.Assert((&AppRoutesRebuild{}).Info(), check.NotNil) 19 | } 20 | 21 | func (s *S) TestAppRoutesRebuildRun(c *check.C) { 22 | var stdout, stderr bytes.Buffer 23 | context := cmd.Context{ 24 | Stdout: &stdout, 25 | Stderr: &stderr, 26 | } 27 | 28 | trans := &cmdtest.ConditionalTransport{ 29 | Transport: cmdtest.Transport{Status: http.StatusOK}, 30 | CondFunc: func(req *http.Request) bool { 31 | return strings.HasSuffix(req.URL.Path, "/apps/app1/routes") && req.Method == "POST" 32 | }, 33 | } 34 | 35 | s.setupFakeTransport(trans) 36 | 37 | command := AppRoutesRebuild{} 38 | command.Flags().Parse(true, []string{"--app", "app1"}) 39 | err := command.Run(&context) 40 | c.Assert(err, check.IsNil) 41 | c.Assert(stdout.String(), check.Equals, `routes was rebuilt successfully 42 | `) 43 | } 44 | 45 | func (s *S) TestAppRoutesRebuildFailed(c *check.C) { 46 | var stdout, stderr bytes.Buffer 47 | context := cmd.Context{ 48 | Stdout: &stdout, 49 | Stderr: &stderr, 50 | } 51 | trans := &cmdtest.ConditionalTransport{ 52 | Transport: cmdtest.Transport{Message: "Some error", Status: http.StatusBadGateway}, 53 | CondFunc: func(req *http.Request) bool { 54 | return strings.HasSuffix(req.URL.Path, "/apps/app1/routes") && req.Method == "POST" 55 | }, 56 | } 57 | s.setupFakeTransport(trans) 58 | 59 | command := AppRoutesRebuild{} 60 | command.Flags().Parse(true, []string{"--app", "app1"}) 61 | err := command.Run(&context) 62 | c.Assert(err, check.Not(check.IsNil)) 63 | c.Assert(err.Error(), check.Matches, ".*: Some error") 64 | } 65 | -------------------------------------------------------------------------------- /tsuru/admin/broker.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/tsuru/gnuflag" 12 | "github.com/tsuru/go-tsuruclient/pkg/tsuru" 13 | "github.com/tsuru/tablecli" 14 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 15 | "github.com/tsuru/tsuru/cmd" 16 | ) 17 | 18 | type BrokerAdd struct { 19 | broker tsuru.ServiceBroker 20 | fs *gnuflag.FlagSet 21 | cacheExpiration string 22 | } 23 | 24 | func (c *BrokerAdd) Info() *cmd.Info { 25 | return &cmd.Info{ 26 | Name: "service-broker-add", 27 | Usage: "service broker add [-i/--insecure] [-c/--context key=value] [-t/--token token] [-u/--user username] [-p/--password password] [--cache cache expiration time]", 28 | Desc: `Adds a new Service Broker.`, 29 | MinArgs: 2, 30 | } 31 | } 32 | 33 | func (c *BrokerAdd) Flags() *gnuflag.FlagSet { 34 | if c.fs == nil { 35 | c.fs = flagsForServiceBroker(&c.broker, &c.cacheExpiration) 36 | } 37 | return c.fs 38 | } 39 | 40 | func (c *BrokerAdd) Run(ctx *cmd.Context) error { 41 | apiClient, err := tsuruHTTP.TsuruClientFromEnvironment() 42 | if err != nil { 43 | return err 44 | } 45 | err = parseServiceBroker(&c.broker, ctx, c.cacheExpiration) 46 | if err != nil { 47 | return err 48 | } 49 | _, err = apiClient.ServiceApi.ServiceBrokerCreate(context.TODO(), c.broker) 50 | if err != nil { 51 | return err 52 | } 53 | fmt.Fprintln(ctx.Stdout, "Service broker successfully added.") 54 | return nil 55 | } 56 | 57 | type BrokerUpdate struct { 58 | broker tsuru.ServiceBroker 59 | fs *gnuflag.FlagSet 60 | cacheExpiration string 61 | noCache bool 62 | } 63 | 64 | func (c *BrokerUpdate) Info() *cmd.Info { 65 | return &cmd.Info{ 66 | Name: "service-broker-update", 67 | Usage: "service broker update [-i/--insecure] [-c/--context key=value] [-t/--token token] [-u/--user username] [-p/--password password] [--cache cache expiration time] [--no-cache]", 68 | Desc: `Updates a service broker.`, 69 | MinArgs: 2, 70 | } 71 | } 72 | 73 | func (c *BrokerUpdate) Flags() *gnuflag.FlagSet { 74 | if c.fs == nil { 75 | c.fs = flagsForServiceBroker(&c.broker, &c.cacheExpiration) 76 | c.fs.BoolVar(&c.noCache, "no-cache", false, "Disable cache expiration config.") 77 | } 78 | return c.fs 79 | } 80 | 81 | func (c *BrokerUpdate) Run(ctx *cmd.Context) error { 82 | apiClient, err := tsuruHTTP.TsuruClientFromEnvironment() 83 | if err != nil { 84 | return err 85 | } 86 | if len(c.cacheExpiration) > 0 && c.noCache { 87 | return fmt.Errorf("can't set --cache and --no-cache flags together") 88 | } 89 | err = parseServiceBroker(&c.broker, ctx, c.cacheExpiration) 90 | if err != nil { 91 | return err 92 | } 93 | if c.noCache { 94 | c.broker.Config.CacheExpirationSeconds = -1 95 | } 96 | _, err = apiClient.ServiceApi.ServiceBrokerUpdate(context.TODO(), c.broker.Name, c.broker) 97 | if err != nil { 98 | return err 99 | } 100 | fmt.Fprintln(ctx.Stdout, "Service broker successfully updated.") 101 | return nil 102 | } 103 | 104 | type BrokerDelete struct{} 105 | 106 | func (c *BrokerDelete) Info() *cmd.Info { 107 | return &cmd.Info{ 108 | Name: "service-broker-delete", 109 | Usage: "service broker delete ", 110 | Desc: `Removes a service broker.`, 111 | MinArgs: 1, 112 | } 113 | } 114 | 115 | func (c *BrokerDelete) Run(ctx *cmd.Context) error { 116 | apiClient, err := tsuruHTTP.TsuruClientFromEnvironment() 117 | if err != nil { 118 | return err 119 | } 120 | _, err = apiClient.ServiceApi.ServiceBrokerDelete(context.TODO(), ctx.Args[0]) 121 | if err != nil { 122 | return err 123 | } 124 | fmt.Fprintln(ctx.Stdout, "Service broker successfully deleted.") 125 | return nil 126 | } 127 | 128 | type BrokerList struct{} 129 | 130 | func (c *BrokerList) Info() *cmd.Info { 131 | return &cmd.Info{ 132 | Name: "service-broker-list", 133 | Usage: "service broker list", 134 | Desc: `List service brokers.`, 135 | } 136 | } 137 | 138 | func (c *BrokerList) Run(ctx *cmd.Context) error { 139 | apiClient, err := tsuruHTTP.TsuruClientFromEnvironment() 140 | if err != nil { 141 | return err 142 | } 143 | brokerList, _, err := apiClient.ServiceApi.ServiceBrokerList(context.TODO()) 144 | if err != nil { 145 | return err 146 | } 147 | tbl := tablecli.Table{ 148 | Headers: tablecli.Row{"Name", "URL", "Insecure", "Auth", "Context"}, 149 | LineSeparator: true, 150 | } 151 | for _, b := range brokerList.Brokers { 152 | authMethod := "None" 153 | if b.Config.AuthConfig.BasicAuthConfig.Username != "" { 154 | authMethod = "Basic\n" 155 | } else if b.Config.AuthConfig.BearerConfig.Token != "" { 156 | authMethod = "Bearer\n" 157 | } 158 | var contexts []string 159 | for k, v := range b.Config.Context { 160 | contexts = append(contexts, fmt.Sprintf("%v: %v", k, v)) 161 | } 162 | sort.Strings(contexts) 163 | tbl.AddRow(tablecli.Row{ 164 | b.Name, 165 | b.URL, 166 | strconv.FormatBool(b.Config.Insecure), 167 | authMethod, 168 | strings.Join(contexts, "\n"), 169 | }) 170 | } 171 | fmt.Fprint(ctx.Stdout, tbl.String()) 172 | return nil 173 | } 174 | 175 | func flagsForServiceBroker(broker *tsuru.ServiceBroker, cacheExpiration *string) *gnuflag.FlagSet { 176 | fs := gnuflag.NewFlagSet("", gnuflag.ExitOnError) 177 | 178 | insecure := "Ignore TLS errors in the broker request." 179 | fs.BoolVar(&broker.Config.Insecure, "insecure", false, insecure) 180 | fs.BoolVar(&broker.Config.Insecure, "i", false, insecure) 181 | 182 | context := "Context values to be sent on every broker request." 183 | fs.Var(cmd.MapFlagWrapper{Dst: &broker.Config.Context}, "context", context) 184 | fs.Var(cmd.MapFlagWrapper{Dst: &broker.Config.Context}, "c", context) 185 | 186 | pass := "Service broker authentication password." 187 | fs.StringVar(&broker.Config.AuthConfig.BasicAuthConfig.Password, "password", "", pass) 188 | fs.StringVar(&broker.Config.AuthConfig.BasicAuthConfig.Password, "p", "", pass) 189 | 190 | user := "Service broker authentication username." 191 | fs.StringVar(&broker.Config.AuthConfig.BasicAuthConfig.Username, "user", "", user) 192 | fs.StringVar(&broker.Config.AuthConfig.BasicAuthConfig.Username, "u", "", user) 193 | 194 | token := "Service broker authentication token." 195 | fs.StringVar(&broker.Config.AuthConfig.BearerConfig.Token, "token", "", token) 196 | fs.StringVar(&broker.Config.AuthConfig.BearerConfig.Token, "t", "", token) 197 | 198 | cache := `Cache expiration time for service broker catalog. This may use a duration notation like "5m" or "2h".` 199 | fs.StringVar(cacheExpiration, "cache", "", cache) 200 | 201 | return fs 202 | } 203 | 204 | func parseServiceBroker(broker *tsuru.ServiceBroker, ctx *cmd.Context, cacheExpiration string) error { 205 | broker.Name, broker.URL = ctx.Args[0], ctx.Args[1] 206 | if len(cacheExpiration) > 0 { 207 | duration, err := time.ParseDuration(cacheExpiration) 208 | if err != nil { 209 | return err 210 | } 211 | broker.Config.CacheExpirationSeconds = int32(duration.Seconds()) 212 | } 213 | return nil 214 | } 215 | -------------------------------------------------------------------------------- /tsuru/admin/event.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package admin 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "sort" 12 | "time" 13 | 14 | "github.com/tsuru/gnuflag" 15 | "github.com/tsuru/go-tsuruclient/pkg/config" 16 | "github.com/tsuru/tablecli" 17 | "github.com/tsuru/tsuru-client/tsuru/formatter" 18 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 19 | "github.com/tsuru/tsuru/cmd" 20 | "github.com/tsuru/tsuru/event" 21 | eventTypes "github.com/tsuru/tsuru/types/event" 22 | ) 23 | 24 | type EventBlockList struct { 25 | fs *gnuflag.FlagSet 26 | active bool 27 | } 28 | 29 | func (c *EventBlockList) Info() *cmd.Info { 30 | return &cmd.Info{ 31 | Name: "event-block-list", 32 | Usage: "event block list [-a/--active]", 33 | Desc: "Lists all event blocks", 34 | MinArgs: 0, 35 | } 36 | } 37 | 38 | func (c *EventBlockList) Flags() *gnuflag.FlagSet { 39 | if c.fs == nil { 40 | c.fs = gnuflag.NewFlagSet("", gnuflag.ExitOnError) 41 | c.fs.BoolVar(&c.active, "active", false, "Display only active blocks.") 42 | c.fs.BoolVar(&c.active, "a", false, "Display only active blocks.") 43 | } 44 | return c.fs 45 | } 46 | 47 | func (c *EventBlockList) Run(context *cmd.Context) error { 48 | path := "/events/blocks" 49 | if c.active { 50 | path += "?active=true" 51 | } 52 | url, err := config.GetURLVersion("1.3", path) 53 | if err != nil { 54 | return err 55 | } 56 | request, _ := http.NewRequest("GET", url, nil) 57 | resp, err := tsuruHTTP.AuthenticatedClient.Do(request) 58 | if err != nil { 59 | return err 60 | } 61 | defer resp.Body.Close() 62 | var blocks []event.Block 63 | if resp.StatusCode == http.StatusOK { 64 | err = json.NewDecoder(resp.Body).Decode(&blocks) 65 | if err != nil { 66 | return err 67 | } 68 | } 69 | tbl := tablecli.NewTable() 70 | tbl.Headers = tablecli.Row{"ID", "Start (duration)", "Kind", "Owner", "Target (Type: Value)", "Conditions", "Reason"} 71 | for _, b := range blocks { 72 | var duration *time.Duration 73 | if !b.EndTime.IsZero() { 74 | timeDiff := b.EndTime.Sub(b.StartTime) 75 | duration = &timeDiff 76 | } 77 | ts := formatter.FormatDateAndDuration(b.StartTime, duration) 78 | kind := valueOrWildcard(b.KindName) 79 | owner := valueOrWildcard(b.OwnerName) 80 | targetType := valueOrWildcard(string(b.Target.Type)) 81 | targetValue := valueOrWildcard(b.Target.Value) 82 | conditions := mapValueOrWildcard(b.Conditions) 83 | row := tablecli.Row{b.ID.Hex(), ts, kind, owner, fmt.Sprintf("%s: %s", targetType, targetValue), conditions, b.Reason} 84 | color := "yellow" 85 | if !b.Active { 86 | color = "white" 87 | } 88 | for i, v := range row { 89 | row[i] = cmd.Colorfy(v, color, "", "") 90 | } 91 | tbl.AddRow(row) 92 | } 93 | context.Stdout.Write([]byte(tbl.String())) 94 | return nil 95 | } 96 | 97 | func valueOrWildcard(str string) string { 98 | if str == "" { 99 | return "all" 100 | } 101 | return str 102 | } 103 | 104 | func mapValueOrWildcard(m map[string]string) string { 105 | if len(m) == 0 { 106 | return "all" 107 | } 108 | 109 | keys := make([]string, 0, len(m)) 110 | for k := range m { 111 | keys = append(keys, k) 112 | } 113 | 114 | sort.Strings(keys) 115 | var s string 116 | for _, k := range keys { 117 | s += fmt.Sprintf("%s=%s ", k, m[k]) 118 | } 119 | 120 | return s[0 : len(s)-1] // trim last space 121 | } 122 | 123 | type EventBlockAdd struct { 124 | fs *gnuflag.FlagSet 125 | kind string 126 | owner string 127 | targetType string 128 | targetValue string 129 | conditions cmd.MapFlag 130 | } 131 | 132 | func (c *EventBlockAdd) Info() *cmd.Info { 133 | return &cmd.Info{ 134 | Name: "event-block-add", 135 | Usage: "event block add [-k/--kind kindName] [-o/--owner ownerName] [-t/--target targetType] [-v/--value targetValue] [-c/--conditions name=value]...", 136 | Desc: "Block events.", 137 | MinArgs: 1, 138 | } 139 | } 140 | 141 | func (c *EventBlockAdd) Flags() *gnuflag.FlagSet { 142 | if c.fs == nil { 143 | c.fs = gnuflag.NewFlagSet("", gnuflag.ExitOnError) 144 | c.fs.StringVar(&c.kind, "kind", "", "Event kind to be blocked.") 145 | c.fs.StringVar(&c.kind, "k", "", "Event kind to be blocked.") 146 | c.fs.StringVar(&c.owner, "owner", "", "Block this owner's events.") 147 | c.fs.StringVar(&c.owner, "o", "", "Block this owner's events.") 148 | c.fs.StringVar(&c.targetType, "target", "", "Block events with this target type.") 149 | c.fs.StringVar(&c.targetType, "t", "", "Block events with this target type.") 150 | c.fs.StringVar(&c.targetValue, "value", "", "Block events with this target value.") 151 | c.fs.StringVar(&c.targetValue, "v", "", "Block events with this target value.") 152 | c.fs.Var(&c.conditions, "conditions", "Conditions to apply on event kind to be blocked.") 153 | c.fs.Var(&c.conditions, "c", "Conditions to apply on event kind to be blocked.") 154 | } 155 | return c.fs 156 | } 157 | 158 | func (c *EventBlockAdd) Run(context *cmd.Context) error { 159 | url, err := config.GetURLVersion("1.3", "/events/blocks") 160 | if err != nil { 161 | return err 162 | } 163 | target := eventTypes.Target{} 164 | if c.targetType != "" { 165 | var targetType eventTypes.TargetType 166 | targetType, err = eventTypes.GetTargetType(c.targetType) 167 | if err != nil { 168 | return err 169 | } 170 | target.Type = targetType 171 | } 172 | target.Value = c.targetValue 173 | block := event.Block{ 174 | Reason: context.Args[0], 175 | KindName: c.kind, 176 | OwnerName: c.owner, 177 | Target: target, 178 | Conditions: c.conditions, 179 | } 180 | 181 | body, err := json.Marshal(block) 182 | if err != nil { 183 | return err 184 | } 185 | err = doRequest(url, http.MethodPost, body) 186 | if err != nil { 187 | return err 188 | } 189 | context.Stdout.Write([]byte("Block successfully added.\n")) 190 | return nil 191 | } 192 | 193 | type EventBlockRemove struct{} 194 | 195 | func (c *EventBlockRemove) Info() *cmd.Info { 196 | return &cmd.Info{ 197 | Name: "event-block-remove", 198 | Usage: "event block remove ", 199 | Desc: "Removes an event block.", 200 | MinArgs: 1, 201 | MaxArgs: 1, 202 | } 203 | } 204 | 205 | func (c *EventBlockRemove) Run(context *cmd.Context) error { 206 | uuid := context.Args[0] 207 | url, err := config.GetURLVersion("1.3", fmt.Sprintf("/events/blocks/%s", uuid)) 208 | if err != nil { 209 | return err 210 | } 211 | request, _ := http.NewRequest(http.MethodDelete, url, nil) 212 | _, err = tsuruHTTP.AuthenticatedClient.Do(request) 213 | if err != nil { 214 | return err 215 | } 216 | fmt.Fprintf(context.Stdout, "Block %s successfully removed.\n", uuid) 217 | return nil 218 | } 219 | -------------------------------------------------------------------------------- /tsuru/admin/event_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package admin 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | "os" 11 | 12 | "github.com/tsuru/tsuru/cmd" 13 | "github.com/tsuru/tsuru/cmd/cmdtest" 14 | "github.com/tsuru/tsuru/event" 15 | eventTypes "github.com/tsuru/tsuru/types/event" 16 | check "gopkg.in/check.v1" 17 | ) 18 | 19 | func (s *S) TestEventBlockListInfo(c *check.C) { 20 | c.Assert((&EventBlockList{}).Info(), check.NotNil) 21 | } 22 | func (s *S) TestEventBlockList(c *check.C) { 23 | os.Setenv("TSURU_DISABLE_COLORS", "1") 24 | defer os.Unsetenv("TSURU_DISABLE_COLORS") 25 | var stdout, stderr bytes.Buffer 26 | context := cmd.Context{ 27 | Args: []string{}, 28 | Stdout: &stdout, 29 | Stderr: &stderr, 30 | } 31 | blocksData := ` 32 | [{ 33 | "ID": "58c6db0b0640fd2fec413cc6", 34 | "StartTime": "2017-03-13T17:46:51.326Z", 35 | "EndTime": "0001-01-01T00:00:00Z", 36 | "KindName": "app.create", 37 | "OwnerName": "user@email.com", 38 | "Target": { 39 | "Type": "", 40 | "Value": "" 41 | }, 42 | "Reason": "Problems", 43 | "Active": true 44 | }, { 45 | "ID": "58c1d29ac47369e95c5520c8", 46 | "StartTime": "2017-03-13T16:43:09.888Z", 47 | "EndTime": "2017-03-13T17:27:25.149Z", 48 | "KindName": "app.deploy", 49 | "OwnerName": "", 50 | "Target": { 51 | "Type": "", 52 | "Value": "" 53 | }, 54 | "Reason": "Maintenance.", 55 | "Conditions": {"pool": "pool1", "cluster": "cluster1"}, 56 | "Active": false 57 | }]` 58 | trans := &cmdtest.ConditionalTransport{ 59 | Transport: cmdtest.Transport{Message: blocksData, Status: http.StatusOK}, 60 | CondFunc: func(req *http.Request) bool { 61 | return req.URL.Path == "/1.3/events/blocks" 62 | }, 63 | } 64 | s.setupFakeTransport(trans) 65 | command := EventBlockList{} 66 | err := command.Run(&context) 67 | c.Assert(err, check.IsNil) 68 | expected := `+--------------------------+-----------------------------+------------+----------------+----------------------+-----------------------------+--------------+ 69 | | ID | Start (duration) | Kind | Owner | Target (Type: Value) | Conditions | Reason | 70 | +--------------------------+-----------------------------+------------+----------------+----------------------+-----------------------------+--------------+ 71 | | 58c6db0b0640fd2fec413cc6 | 13 Mar 17 12:46 CDT (…) | app.create | user@email.com | all: all | all | Problems | 72 | | 58c1d29ac47369e95c5520c8 | 13 Mar 17 11:43 CDT (44:15) | app.deploy | all | all: all | cluster=cluster1 pool=pool1 | Maintenance. | 73 | +--------------------------+-----------------------------+------------+----------------+----------------------+-----------------------------+--------------+ 74 | ` 75 | c.Assert(stdout.String(), check.Equals, expected) 76 | } 77 | 78 | func (s *S) TestEventBlockListNoEvents(c *check.C) { 79 | os.Setenv("TSURU_DISABLE_COLORS", "1") 80 | defer os.Unsetenv("TSURU_DISABLE_COLORS") 81 | var stdout, stderr bytes.Buffer 82 | context := cmd.Context{ 83 | Args: []string{}, 84 | Stdout: &stdout, 85 | Stderr: &stderr, 86 | } 87 | trans := &cmdtest.ConditionalTransport{ 88 | Transport: cmdtest.Transport{Status: http.StatusNoContent}, 89 | CondFunc: func(req *http.Request) bool { 90 | return req.URL.Path == "/1.3/events/blocks" 91 | }, 92 | } 93 | s.setupFakeTransport(trans) 94 | command := EventBlockList{} 95 | err := command.Run(&context) 96 | c.Assert(err, check.IsNil) 97 | expected := `+----+------------------+------+-------+----------------------+------------+--------+ 98 | | ID | Start (duration) | Kind | Owner | Target (Type: Value) | Conditions | Reason | 99 | +----+------------------+------+-------+----------------------+------------+--------+ 100 | +----+------------------+------+-------+----------------------+------------+--------+ 101 | ` 102 | c.Assert(stdout.String(), check.Equals, expected) 103 | } 104 | 105 | func (s *S) TestEventBlockAddInfo(c *check.C) { 106 | c.Assert((&EventBlockAdd{}).Info(), check.NotNil) 107 | } 108 | func (s *S) TestEventBlockAdd(c *check.C) { 109 | var stdout, stderr bytes.Buffer 110 | context := cmd.Context{ 111 | Args: []string{"Reason"}, 112 | Stdout: &stdout, 113 | Stderr: &stderr, 114 | } 115 | trans := &cmdtest.ConditionalTransport{ 116 | Transport: cmdtest.Transport{Message: "", Status: http.StatusOK}, 117 | CondFunc: func(req *http.Request) bool { 118 | block := new(event.Block) 119 | decodeJSONBody(c, req, block) 120 | c.Assert(block, check.DeepEquals, &event.Block{Reason: "Reason", Active: false}) 121 | return req.URL.Path == "/1.3/events/blocks" && req.Method == http.MethodPost 122 | }, 123 | } 124 | s.setupFakeTransport(trans) 125 | command := EventBlockAdd{} 126 | err := command.Run(&context) 127 | c.Assert(err, check.IsNil) 128 | c.Assert(stdout.String(), check.Equals, "Block successfully added.\n") 129 | } 130 | 131 | func (s *S) TestEventBlockAddAllFlags(c *check.C) { 132 | var stdout, stderr bytes.Buffer 133 | context := cmd.Context{ 134 | Args: []string{"Reason"}, 135 | Stdout: &stdout, 136 | Stderr: &stderr, 137 | } 138 | trans := &cmdtest.ConditionalTransport{ 139 | Transport: cmdtest.Transport{Message: "", Status: http.StatusOK}, 140 | CondFunc: func(req *http.Request) bool { 141 | block := new(event.Block) 142 | decodeJSONBody(c, req, block) 143 | c.Assert(block, check.DeepEquals, &event.Block{ 144 | KindName: "app.deploy", 145 | OwnerName: "user@email.com", 146 | Target: eventTypes.Target{Type: eventTypes.TargetTypeApp, Value: "myapp"}, 147 | Reason: "Reason", 148 | Active: false, 149 | }) 150 | return req.URL.Path == "/1.3/events/blocks" && req.Method == http.MethodPost 151 | }, 152 | } 153 | s.setupFakeTransport(trans) 154 | command := EventBlockAdd{} 155 | command.Flags().Parse(true, []string{"-k", "app.deploy", "-o", "user@email.com", "-t", "app", "-v", "myapp"}) 156 | err := command.Run(&context) 157 | c.Assert(err, check.IsNil) 158 | c.Assert(stdout.String(), check.Equals, "Block successfully added.\n") 159 | } 160 | 161 | func (s *S) TestEventBlockRemoveInfo(c *check.C) { 162 | c.Assert((&EventBlockRemove{}).Info(), check.NotNil) 163 | } 164 | func (s *S) TestEventBlockRemove(c *check.C) { 165 | var stdout, stderr bytes.Buffer 166 | context := cmd.Context{ 167 | Args: []string{"ABC123K12"}, 168 | Stdout: &stdout, 169 | Stderr: &stderr, 170 | } 171 | trans := &cmdtest.ConditionalTransport{ 172 | Transport: cmdtest.Transport{Message: "", Status: http.StatusOK}, 173 | CondFunc: func(req *http.Request) bool { 174 | return req.URL.Path == "/1.3/events/blocks/ABC123K12" && req.Method == http.MethodDelete 175 | }, 176 | } 177 | s.setupFakeTransport(trans) 178 | command := EventBlockRemove{} 179 | err := command.Run(&context) 180 | c.Assert(err, check.IsNil) 181 | c.Assert(stdout.String(), check.Equals, "Block ABC123K12 successfully removed.\n") 182 | } 183 | -------------------------------------------------------------------------------- /tsuru/admin/plan.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package admin 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/tsuru/gnuflag" 15 | "github.com/tsuru/go-tsuruclient/pkg/config" 16 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 17 | "github.com/tsuru/tsuru/cmd" 18 | "k8s.io/apimachinery/pkg/api/resource" 19 | ) 20 | 21 | type PlanCreate struct { 22 | memory string 23 | cpu string 24 | setDefault bool 25 | fs *gnuflag.FlagSet 26 | } 27 | 28 | func (c *PlanCreate) Flags() *gnuflag.FlagSet { 29 | if c.fs == nil { 30 | c.fs = gnuflag.NewFlagSet("plan-create", gnuflag.ExitOnError) 31 | memory := `Amount of available memory for units in bytes or an integer value followed 32 | by M, K or G for megabytes, kilobytes or gigabytes respectively.` 33 | c.fs.StringVar(&c.memory, "memory", "0", memory) 34 | c.fs.StringVar(&c.memory, "m", "0", memory) 35 | 36 | cpu := `Relative cpu each unit will have available.` 37 | c.fs.StringVar(&c.cpu, "cpu", "0", cpu) 38 | c.fs.StringVar(&c.cpu, "c", "0", cpu) 39 | setDefault := `Set plan as default, this will remove the default flag from any other plan. 40 | The default plan will be used when creating an application without explicitly 41 | setting a plan.` 42 | c.fs.BoolVar(&c.setDefault, "default", false, setDefault) 43 | c.fs.BoolVar(&c.setDefault, "d", false, setDefault) 44 | } 45 | return c.fs 46 | } 47 | 48 | func (c *PlanCreate) Info() *cmd.Info { 49 | return &cmd.Info{ 50 | Name: "plan-create", 51 | Usage: "plan create -c cpu [-m memory] [--default]", 52 | Desc: `Creates a new plan for being used when creating apps.`, 53 | MinArgs: 1, 54 | } 55 | } 56 | 57 | func (c *PlanCreate) Run(context *cmd.Context) error { 58 | u, err := config.GetURL("/plans") 59 | if err != nil { 60 | return err 61 | } 62 | v := url.Values{} 63 | v.Set("name", context.Args[0]) 64 | 65 | memoryValue, err := parseMemoryQuantity(c.memory) 66 | if err != nil { 67 | return err 68 | } 69 | v.Set("memory", fmt.Sprintf("%d", memoryValue)) 70 | 71 | cpuValue, err := parseCPUQuantity(c.cpu) 72 | if err != nil { 73 | return err 74 | } 75 | v.Set("cpumilli", fmt.Sprintf("%d", cpuValue)) 76 | 77 | v.Set("default", strconv.FormatBool(c.setDefault)) 78 | b := strings.NewReader(v.Encode()) 79 | request, err := http.NewRequest("POST", u, b) 80 | if err != nil { 81 | return err 82 | } 83 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded") 84 | _, err = tsuruHTTP.AuthenticatedClient.Do(request) 85 | if err != nil { 86 | fmt.Fprintf(context.Stdout, "Failed to create plan!\n") 87 | return err 88 | } 89 | fmt.Fprintf(context.Stdout, "Plan successfully created!\n") 90 | return nil 91 | } 92 | 93 | type PlanRemove struct{} 94 | 95 | func (c *PlanRemove) Info() *cmd.Info { 96 | return &cmd.Info{ 97 | Name: "plan-remove", 98 | Usage: "plan remove ", 99 | Desc: `Removes an existing plan. It will no longer be available for newly created 100 | apps. However, this won't change anything for existing apps that were created 101 | using the removed plan. They will keep using the same value amount of 102 | resources described by the plan.`, 103 | MinArgs: 1, 104 | } 105 | } 106 | 107 | func (c *PlanRemove) Run(context *cmd.Context) error { 108 | url, err := config.GetURL("/plans/" + context.Args[0]) 109 | if err != nil { 110 | return err 111 | } 112 | request, err := http.NewRequest(http.MethodDelete, url, nil) 113 | if err != nil { 114 | return err 115 | } 116 | _, err = tsuruHTTP.AuthenticatedClient.Do(request) 117 | if err != nil { 118 | fmt.Fprintf(context.Stdout, "Failed to remove plan!\n") 119 | return err 120 | } 121 | fmt.Fprintf(context.Stdout, "Plan successfully removed!\n") 122 | return nil 123 | } 124 | 125 | func parseMemoryQuantity(userQuantity string) (numBytes int64, err error) { 126 | if v, parseErr := strconv.Atoi(userQuantity); parseErr == nil { 127 | return int64(v), nil 128 | } 129 | memoryQuantity, err := resource.ParseQuantity(userQuantity) 130 | if err != nil { 131 | return 0, err 132 | } 133 | 134 | numBytes, _ = memoryQuantity.AsInt64() 135 | return numBytes, nil 136 | } 137 | 138 | func parseCPUQuantity(userQuantity string) (numMillis int64, err error) { 139 | var v int 140 | if v, err = strconv.Atoi(userQuantity); err == nil { 141 | return int64(v) * 1000, nil 142 | } 143 | 144 | if strings.HasSuffix(userQuantity, "%") { 145 | v, err = strconv.Atoi(userQuantity[0 : len(userQuantity)-1]) 146 | if err != nil { 147 | return 0, err 148 | } 149 | return int64(v) * 10, nil 150 | } 151 | 152 | var cpuQuantity resource.Quantity 153 | cpuQuantity, err = resource.ParseQuantity(userQuantity) 154 | 155 | if err != nil { 156 | return 0, err 157 | } 158 | 159 | cpu := cpuQuantity.AsApproximateFloat64() 160 | 161 | return int64(cpu * 1000), nil 162 | } 163 | -------------------------------------------------------------------------------- /tsuru/admin/suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package admin 6 | 7 | import ( 8 | "net/http" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/cezarsa/form" 14 | "github.com/tsuru/tsuru-client/tsuru/formatter" 15 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 16 | check "gopkg.in/check.v1" 17 | ) 18 | 19 | type S struct { 20 | //manager *cmd.Manager 21 | defaultLocation time.Location 22 | } 23 | 24 | func (s *S) SetUpSuite(c *check.C) { 25 | //var stdout, stderr bytes.Buffer 26 | //s.manager = cmd.NewManagerPanicExiter("glb", "1.0.0", "Supported-Tsuru-Version", &stdout, &stderr, os.Stdin, nil) 27 | os.Setenv("TSURU_TARGET", "http://localhost") 28 | form.DefaultEncoder = form.DefaultEncoder.UseJSONTags(false) 29 | form.DefaultDecoder = form.DefaultDecoder.UseJSONTags(false) 30 | } 31 | 32 | func (s *S) TearDownSuite(c *check.C) { 33 | os.Unsetenv("TSURU_TARGET") 34 | } 35 | 36 | func (s *S) SetUpTest(c *check.C) { 37 | s.defaultLocation = *formatter.LocalTZ 38 | location, err := time.LoadLocation("US/Central") 39 | if err == nil { 40 | formatter.LocalTZ = location 41 | } 42 | } 43 | 44 | func (s *S) TearDownTest(c *check.C) { 45 | formatter.LocalTZ = &s.defaultLocation 46 | tsuruHTTP.AuthenticatedClient = &http.Client{} 47 | } 48 | 49 | func (s *S) setupFakeTransport(rt http.RoundTripper) { 50 | tsuruHTTP.AuthenticatedClient = tsuruHTTP.NewTerminalClient(tsuruHTTP.TerminalClientOptions{ 51 | RoundTripper: rt, 52 | ClientName: "test", 53 | ClientVersion: "0.1.0", 54 | }) 55 | } 56 | 57 | var _ = check.Suite(&S{}) 58 | 59 | func Test(t *testing.T) { check.TestingT(t) } 60 | -------------------------------------------------------------------------------- /tsuru/admin/testdata/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tsuru/java 2 | RUN true 3 | -------------------------------------------------------------------------------- /tsuru/admin/testdata/cacert.pem: -------------------------------------------------------------------------------- 1 | invalidcacert -------------------------------------------------------------------------------- /tsuru/admin/testdata/cert.pem: -------------------------------------------------------------------------------- 1 | invalidcert -------------------------------------------------------------------------------- /tsuru/admin/testdata/doc.md: -------------------------------------------------------------------------------- 1 | my doc -------------------------------------------------------------------------------- /tsuru/admin/testdata/key.pem: -------------------------------------------------------------------------------- 1 | invalidkey -------------------------------------------------------------------------------- /tsuru/admin/testdata/manifest.yml: -------------------------------------------------------------------------------- 1 | id: mysqlapi 2 | endpoint: 3 | production: mysqlapi.com 4 | -------------------------------------------------------------------------------- /tsuru/app/app.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package app 6 | 7 | import ( 8 | "github.com/pkg/errors" 9 | "github.com/tsuru/gnuflag" 10 | ) 11 | 12 | type AppNameMixIn struct { 13 | fs *gnuflag.FlagSet 14 | appName string 15 | } 16 | 17 | func (cmd *AppNameMixIn) AppNameByArgsAndFlag(args []string) (string, error) { 18 | if len(args) > 0 { 19 | if cmd.appName != "" { 20 | return "", errors.New("You can't use the app flag and specify the app name as an argument at the same time.") 21 | } 22 | 23 | return args[0], nil 24 | } 25 | 26 | return cmd.AppNameByFlag() 27 | } 28 | 29 | func (cmd *AppNameMixIn) AppNameByFlag() (string, error) { 30 | if cmd.appName == "" { 31 | return "", errors.Errorf(`The name of the app is required. 32 | 33 | Use the --app flag to specify it. 34 | 35 | `) 36 | } 37 | return cmd.appName, nil 38 | } 39 | 40 | func (cmd *AppNameMixIn) Flags() *gnuflag.FlagSet { 41 | if cmd.fs == nil { 42 | cmd.fs = gnuflag.NewFlagSet("", gnuflag.ExitOnError) 43 | cmd.fs.StringVar(&cmd.appName, "app", "", "The name of the app.") 44 | cmd.fs.StringVar(&cmd.appName, "a", "", "The name of the app.") 45 | } 46 | return cmd.fs 47 | } 48 | -------------------------------------------------------------------------------- /tsuru/app/app_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package app 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/tsuru/gnuflag" 11 | check "gopkg.in/check.v1" 12 | ) 13 | 14 | var appflag = &gnuflag.Flag{ 15 | Name: "app", 16 | Usage: "The name of the app.", 17 | Value: nil, 18 | DefValue: "", 19 | } 20 | 21 | var appshortflag = &gnuflag.Flag{ 22 | Name: "a", 23 | Usage: "The name of the app.", 24 | Value: nil, 25 | DefValue: "", 26 | } 27 | 28 | func Test(t *testing.T) { check.TestingT(t) } 29 | 30 | type S struct{} 31 | 32 | var _ = check.Suite(&S{}) 33 | 34 | func (s *S) TestAppNameMixInWithFlagDefined(c *check.C) { 35 | g := AppNameMixIn{} 36 | g.Flags().Parse(true, []string{"--app", "myapp"}) 37 | name, err := g.AppNameByFlag() 38 | c.Assert(err, check.IsNil) 39 | c.Assert(name, check.Equals, "myapp") 40 | } 41 | 42 | func (s *S) TestAppNameMixInWithShortFlagDefined(c *check.C) { 43 | g := AppNameMixIn{} 44 | g.Flags().Parse(true, []string{"-a", "myapp"}) 45 | name, err := g.AppNameByFlag() 46 | c.Assert(err, check.IsNil) 47 | c.Assert(name, check.Equals, "myapp") 48 | } 49 | 50 | func (s *S) TestAppNameMixInArgs(c *check.C) { 51 | g := AppNameMixIn{} 52 | g.Flags().Parse(true, []string{}) 53 | name, err := g.AppNameByArgsAndFlag([]string{"myapp"}) 54 | c.Assert(err, check.IsNil) 55 | c.Assert(name, check.Equals, "myapp") 56 | } 57 | 58 | func (s *S) TestAppNameMixInArgsConflict(c *check.C) { 59 | g := AppNameMixIn{} 60 | g.Flags().Parse(true, []string{"-a", "myapp"}) 61 | _, err := g.AppNameByArgsAndFlag([]string{"myapp2"}) 62 | c.Assert(err, check.Not(check.IsNil)) 63 | c.Assert(err.Error(), check.Equals, "You can't use the app flag and specify the app name as an argument at the same time.") 64 | } 65 | 66 | func (s *S) TestAppNameMixInWithoutFlagDefinedFails(c *check.C) { 67 | g := AppNameMixIn{} 68 | name, err := g.AppNameByFlag() 69 | c.Assert(name, check.Equals, "") 70 | c.Assert(err, check.NotNil) 71 | c.Assert(err.Error(), check.Equals, `The name of the app is required. 72 | 73 | Use the --app flag to specify it. 74 | 75 | `) 76 | } 77 | 78 | func (s *S) TestAppNameMixInFlags(c *check.C) { 79 | var flags []gnuflag.Flag 80 | expected := []gnuflag.Flag{*appshortflag, *appflag} 81 | command := AppNameMixIn{} 82 | flagset := command.Flags() 83 | flagset.VisitAll(func(f *gnuflag.Flag) { 84 | f.Value = nil 85 | flags = append(flags, *f) 86 | }) 87 | c.Assert(flags, check.DeepEquals, expected) 88 | } 89 | -------------------------------------------------------------------------------- /tsuru/auth/login.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package auth 6 | 7 | import ( 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "net/http" 13 | "os" 14 | "strings" 15 | 16 | "github.com/tsuru/gnuflag" 17 | "github.com/tsuru/go-tsuruclient/pkg/config" 18 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 19 | "github.com/tsuru/tsuru/cmd" 20 | authTypes "github.com/tsuru/tsuru/types/auth" 21 | ) 22 | 23 | var errTsuruTokenDefined = errors.New("this command can't run with $TSURU_TOKEN environment variable set. Did you forget to unset?") 24 | 25 | type Login struct { 26 | fs *gnuflag.FlagSet 27 | 28 | scheme string 29 | } 30 | 31 | func (c *Login) Info() *cmd.Info { 32 | return &cmd.Info{ 33 | Name: "login", 34 | Usage: "login [email]", 35 | Desc: `Initiates a new tsuru session for a user. If using tsuru native authentication 36 | scheme, it will ask for the email and the password and check if the user is 37 | successfully authenticated. If using OAuth, it will open a web browser for the 38 | user to complete the login. 39 | 40 | After that, the token generated by the tsuru server will be stored in 41 | [[${HOME}/.tsuru/token]]. 42 | 43 | All tsuru actions require the user to be authenticated (except [[tsuru login]] 44 | and [[tsuru version]]).`, 45 | MinArgs: 0, 46 | } 47 | } 48 | 49 | func (c *Login) Flags() *gnuflag.FlagSet { 50 | if c.fs == nil { 51 | c.fs = gnuflag.NewFlagSet("login", gnuflag.ExitOnError) 52 | desc := `Login with specific auth scheme` 53 | c.fs.StringVar(&c.scheme, "scheme", "", desc) 54 | c.fs.StringVar(&c.scheme, "s", "", desc) 55 | } 56 | return c.fs 57 | } 58 | 59 | func (c *Login) Run(ctx *cmd.Context) error { 60 | if os.Getenv("TSURU_TOKEN") != "" { 61 | return errTsuruTokenDefined 62 | } 63 | 64 | scheme, err := getScheme(c.scheme) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | switch scheme.Name { 70 | case "oidc": 71 | return oidcLogin(ctx, scheme) 72 | case "oauth": 73 | return oauthLogin(ctx, scheme) 74 | case "native": 75 | return nativeLogin(ctx) 76 | } 77 | 78 | return fmt.Errorf("scheme %q is not implemented", scheme.Name) 79 | } 80 | 81 | func port(loginInfo *authTypes.SchemeInfo) string { 82 | if loginInfo.Data.Port != "" { 83 | return fmt.Sprintf(":%s", loginInfo.Data.Port) 84 | } 85 | return ":0" 86 | } 87 | 88 | func getScheme(schemeName string) (*authTypes.SchemeInfo, error) { 89 | schemes, err := schemesInfo() 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | foundSchemes := []string{} 95 | 96 | if schemeName == "" { 97 | for _, scheme := range schemes { 98 | if scheme.Default { 99 | return &scheme, nil 100 | } 101 | } 102 | } 103 | 104 | for _, scheme := range schemes { 105 | foundSchemes = append(foundSchemes, scheme.Name) 106 | if scheme.Name == schemeName { 107 | return &scheme, nil 108 | } 109 | } 110 | 111 | if len(foundSchemes) == 0 { 112 | return nil, fmt.Errorf("scheme %q is not found", schemeName) 113 | } 114 | 115 | return nil, fmt.Errorf("scheme %q is not found, valid schemes are: %s", schemeName, strings.Join(foundSchemes, ", ")) 116 | } 117 | 118 | func schemesInfo() ([]authTypes.SchemeInfo, error) { 119 | url, err := config.GetURLVersion("1.18", "/auth/schemes") 120 | if err != nil { 121 | return nil, err 122 | } 123 | resp, err := tsuruHTTP.UnauthenticatedClient.Get(url) 124 | if err != nil { 125 | return nil, err 126 | } 127 | defer resp.Body.Close() 128 | if resp.StatusCode != http.StatusOK { 129 | return nil, fmt.Errorf("could not call %q, status code: %d", url, resp.StatusCode) 130 | } 131 | 132 | schemes := []authTypes.SchemeInfo{} 133 | err = json.NewDecoder(resp.Body).Decode(&schemes) 134 | if err != nil { 135 | return nil, err 136 | } 137 | return schemes, nil 138 | } 139 | 140 | const callbackPage = ` 141 | 142 | 143 | 148 | 149 | 150 | %s 151 | 152 | 153 | ` 154 | 155 | const successMarkup = ` 156 | 157 |

Login Successful!

158 |

You can close this window now.

159 | ` 160 | 161 | const errorMarkup = ` 162 |

Login Failed!

163 |

%s

164 | ` 165 | 166 | func writeHTMLError(w io.Writer, err error) { 167 | msg := fmt.Sprintf(errorMarkup, err.Error()) 168 | fmt.Fprintf(w, callbackPage, msg) 169 | } 170 | -------------------------------------------------------------------------------- /tsuru/auth/login_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | package auth 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | "net/http" 10 | "os" 11 | "strings" 12 | 13 | "github.com/tsuru/go-tsuruclient/pkg/config" 14 | "github.com/tsuru/tsuru/cmd" 15 | "github.com/tsuru/tsuru/cmd/cmdtest" 16 | "github.com/tsuru/tsuru/fs/fstest" 17 | "github.com/tsuru/tsuru/types/auth" 18 | "gopkg.in/check.v1" 19 | ) 20 | 21 | func targetInit() { 22 | f, _ := config.Filesystem().Create(config.JoinWithUserDir(".tsuru", "target")) 23 | f.Write([]byte("http://localhost")) 24 | f.Close() 25 | config.WriteOnTargetList("test", "http://localhost") 26 | } 27 | 28 | func setupNativeScheme(trans http.RoundTripper) { 29 | setupFakeTransport(&cmdtest.MultiConditionalTransport{ 30 | ConditionalTransports: []cmdtest.ConditionalTransport{ 31 | { 32 | Transport: cmdtest.Transport{ 33 | Message: `[{"name": "native", "default": true}]`, 34 | Status: http.StatusOK, 35 | }, 36 | CondFunc: func(r *http.Request) bool { 37 | return strings.HasSuffix(r.URL.Path, "/1.18/auth/schemes") 38 | }, 39 | }, 40 | { 41 | Transport: trans, 42 | CondFunc: func(r *http.Request) bool { 43 | return trans != nil 44 | }, 45 | }, 46 | }, 47 | }) 48 | } 49 | 50 | func (s *S) TestNativeLogin(c *check.C) { 51 | os.Unsetenv("TSURU_TOKEN") 52 | config.SetFileSystem(&fstest.RecordingFs{FileContent: "old-token"}) 53 | targetInit() 54 | defer func() { 55 | config.ResetFileSystem() 56 | }() 57 | expected := "Password: \nSuccessfully logged in!\n" 58 | reader := strings.NewReader("chico\n") 59 | context := cmd.Context{ 60 | Args: []string{"foo@foo.com"}, 61 | Stdout: bytes.NewBufferString(""), 62 | Stderr: io.Discard, 63 | Stdin: reader, 64 | } 65 | transport := cmdtest.ConditionalTransport{ 66 | Transport: cmdtest.Transport{ 67 | Message: `{"token": "sometoken", "is_admin": true}`, 68 | Status: http.StatusOK, 69 | }, 70 | CondFunc: func(r *http.Request) bool { 71 | contentType := r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" 72 | password := r.FormValue("password") == "chico" 73 | url := r.URL.Path == "/1.0/users/foo@foo.com/tokens" 74 | return contentType && password && url 75 | }, 76 | } 77 | 78 | command := Login{} 79 | setupNativeScheme(&transport) 80 | err := command.Run(&context) 81 | c.Assert(err, check.IsNil) 82 | c.Assert(context.Stdout.(*bytes.Buffer).String(), check.Equals, expected) 83 | token, err := config.ReadTokenV1() 84 | c.Assert(err, check.IsNil) 85 | c.Assert(token, check.Equals, "sometoken") 86 | } 87 | 88 | func (s *S) TestNativeLoginWithoutEmailFromArg(c *check.C) { 89 | os.Unsetenv("TSURU_TOKEN") 90 | config.SetFileSystem(&fstest.RecordingFs{}) 91 | targetInit() 92 | defer func() { 93 | config.ResetFileSystem() 94 | }() 95 | expected := "Email: Password: \nSuccessfully logged in!\n" 96 | reader := strings.NewReader("chico@tsuru.io\nchico\n") 97 | context := cmd.Context{ 98 | Args: []string{}, 99 | Stdout: bytes.NewBufferString(""), 100 | Stderr: io.Discard, 101 | Stdin: reader, 102 | } 103 | transport := cmdtest.ConditionalTransport{ 104 | Transport: cmdtest.Transport{ 105 | Message: `{"token": "sometoken", "is_admin": true}`, 106 | Status: http.StatusOK, 107 | }, 108 | CondFunc: func(r *http.Request) bool { 109 | return r.URL.Path == "/1.0/users/chico@tsuru.io/tokens" 110 | }, 111 | } 112 | setupNativeScheme(&transport) 113 | command := Login{} 114 | err := command.Run(&context) 115 | c.Assert(err, check.IsNil) 116 | c.Assert(context.Stdout.(*bytes.Buffer).String(), check.Equals, expected) 117 | token, err := config.ReadTokenV1() 118 | c.Assert(err, check.IsNil) 119 | c.Assert(token, check.Equals, "sometoken") 120 | } 121 | 122 | func (s *S) TestNativeLoginShouldNotDependOnTsuruTokenFile(c *check.C) { 123 | oldToken := os.Getenv("TSURU_TOKEN") 124 | os.Unsetenv("TSURU_TOKEN") 125 | defer func() { 126 | os.Setenv("TSURU_TOKEN", oldToken) 127 | }() 128 | config.SetFileSystem(&fstest.RecordingFs{}) 129 | defer func() { 130 | config.ResetFileSystem() 131 | }() 132 | f, _ := config.Filesystem().Create(config.JoinWithUserDir(".tsuru", "target")) 133 | f.Write([]byte("http://localhost")) 134 | f.Close() 135 | expected := "Password: \nSuccessfully logged in!\n" 136 | reader := strings.NewReader("chico\n") 137 | context := cmd.Context{ 138 | Args: []string{"foo@foo.com"}, 139 | Stdout: bytes.NewBufferString(""), 140 | Stderr: io.Discard, 141 | Stdin: reader, 142 | } 143 | setupNativeScheme(&cmdtest.Transport{Message: `{"token":"anothertoken"}`, Status: http.StatusOK}) 144 | command := Login{} 145 | err := command.Run(&context) 146 | c.Assert(err, check.IsNil) 147 | c.Assert(context.Stdout.(*bytes.Buffer).String(), check.Equals, expected) 148 | } 149 | 150 | func (s *S) TestNativeLoginShouldReturnErrorIfThePasswordIsNotGiven(c *check.C) { 151 | oldToken := os.Getenv("TSURU_TOKEN") 152 | os.Unsetenv("TSURU_TOKEN") 153 | config.SetFileSystem(&fstest.RecordingFs{}) 154 | defer func() { 155 | config.ResetFileSystem() 156 | os.Setenv("TSURU_TOKEN", oldToken) 157 | }() 158 | targetInit() 159 | setupNativeScheme(nil) 160 | context := cmd.Context{ 161 | Args: []string{"foo@foo.com"}, 162 | Stdout: io.Discard, 163 | Stderr: io.Discard, 164 | Stdin: strings.NewReader("\n"), 165 | } 166 | command := Login{} 167 | err := command.Run(&context) 168 | c.Assert(err, check.NotNil) 169 | c.Assert(err, check.ErrorMatches, "^You must provide the password!$") 170 | } 171 | 172 | func (s *S) TestNativeLoginWithTsuruToken(c *check.C) { 173 | oldToken := os.Getenv("TSURU_TOKEN") 174 | os.Setenv("TSURU_TOKEN", "settoken") 175 | defer func() { 176 | os.Setenv("TSURU_TOKEN", oldToken) 177 | }() 178 | context := cmd.Context{ 179 | Args: []string{"foo@foo.com"}, 180 | Stdout: io.Discard, 181 | Stderr: io.Discard, 182 | Stdin: strings.NewReader("\n"), 183 | } 184 | command := Login{} 185 | err := command.Run(&context) 186 | c.Assert(err, check.NotNil) 187 | c.Assert(err.Error(), check.Equals, "this command can't run with $TSURU_TOKEN environment variable set. Did you forget to unset?") 188 | } 189 | 190 | func (s *S) TestPort(c *check.C) { 191 | c.Assert(":0", check.Equals, port(&auth.SchemeInfo{})) 192 | c.Assert(":4242", check.Equals, port(&auth.SchemeInfo{Data: auth.SchemeData{Port: "4242"}})) 193 | } 194 | -------------------------------------------------------------------------------- /tsuru/auth/logout.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package auth 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | 11 | "github.com/tsuru/go-tsuruclient/pkg/config" 12 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 13 | "github.com/tsuru/tsuru/cmd" 14 | ) 15 | 16 | type Logout struct{} 17 | 18 | func (c *Logout) Info() *cmd.Info { 19 | return &cmd.Info{ 20 | Name: "logout", 21 | Usage: "logout", 22 | Desc: "Logout will terminate the session with the tsuru server.", 23 | } 24 | } 25 | 26 | func (c *Logout) Run(context *cmd.Context) error { 27 | if url, err := config.GetURL("/users/tokens"); err == nil { 28 | request, _ := http.NewRequest("DELETE", url, nil) 29 | tsuruHTTP.AuthenticatedClient.Do(request) 30 | } 31 | 32 | err := config.RemoveTokenV1() 33 | if err != nil { 34 | return err 35 | } 36 | err = config.RemoveTokenV2() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | fmt.Fprintln(context.Stdout, "Successfully logged out!") 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /tsuru/auth/logout_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | package auth 5 | 6 | import ( 7 | "bytes" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/tsuru/go-tsuruclient/pkg/config" 12 | "github.com/tsuru/tsuru/cmd" 13 | "github.com/tsuru/tsuru/cmd/cmdtest" 14 | "github.com/tsuru/tsuru/fs/fstest" 15 | "gopkg.in/check.v1" 16 | ) 17 | 18 | func (s *S) TestLogout(c *check.C) { 19 | var called bool 20 | rfs := &fstest.RecordingFs{} 21 | config.SetFileSystem(rfs) 22 | defer func() { 23 | config.ResetFileSystem() 24 | }() 25 | config.WriteTokenV1("mytoken") 26 | os.Setenv("TSURU_TARGET", "localhost:8080") 27 | expected := "Successfully logged out!\n" 28 | context := cmd.Context{ 29 | Args: []string{}, 30 | Stdout: bytes.NewBufferString(""), 31 | } 32 | command := Logout{} 33 | transport := cmdtest.ConditionalTransport{ 34 | Transport: cmdtest.Transport{ 35 | Message: "", 36 | Status: http.StatusOK, 37 | }, 38 | CondFunc: func(req *http.Request) bool { 39 | called = true 40 | return req.Method == "DELETE" && req.URL.Path == "/users/tokens" && 41 | req.Header.Get("Authorization") == "bearer mytoken" 42 | }, 43 | } 44 | setupFakeTransport(&transport) 45 | err := command.Run(&context) 46 | c.Assert(err, check.IsNil) 47 | c.Assert(context.Stdout.(*bytes.Buffer).String(), check.Equals, expected) 48 | c.Assert(rfs.HasAction("remove "+config.JoinWithUserDir(".tsuru", "token")), check.Equals, true) 49 | c.Assert(called, check.Equals, true) 50 | } 51 | 52 | func (s *S) TestLogoutNoTarget(c *check.C) { 53 | rfs := &fstest.RecordingFs{} 54 | config.SetFileSystem(rfs) 55 | defer func() { 56 | config.ResetFileSystem() 57 | }() 58 | config.WriteTokenV1("mytoken") 59 | expected := "Successfully logged out!\n" 60 | context := cmd.Context{ 61 | Args: []string{}, 62 | Stdout: bytes.NewBufferString(""), 63 | } 64 | command := Logout{} 65 | transport := cmdtest.Transport{Message: "", Status: http.StatusOK} 66 | setupFakeTransport(transport) 67 | err := command.Run(&context) 68 | c.Assert(err, check.IsNil) 69 | c.Assert(context.Stdout.(*bytes.Buffer).String(), check.Equals, expected) 70 | c.Assert(rfs.HasAction("remove "+config.JoinWithUserDir(".tsuru", "token")), check.Equals, true) 71 | } 72 | -------------------------------------------------------------------------------- /tsuru/auth/native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package auth 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "net/url" 13 | "strings" 14 | 15 | "github.com/tsuru/go-tsuruclient/pkg/config" 16 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 17 | "github.com/tsuru/tsuru/cmd" 18 | ) 19 | 20 | func nativeLogin(ctx *cmd.Context) error { 21 | var email string 22 | 23 | // Use raw output to avoid missing the input prompt messages 24 | ctx.RawOutput() 25 | 26 | if len(ctx.Args) > 0 { 27 | email = ctx.Args[0] 28 | } else { 29 | fmt.Fprint(ctx.Stdout, "Email: ") 30 | fmt.Fscanf(ctx.Stdin, "%s\n", &email) 31 | } 32 | fmt.Fprint(ctx.Stdout, "Password: ") 33 | password, err := cmd.PasswordFromReader(ctx.Stdin) 34 | if err != nil { 35 | return err 36 | } 37 | fmt.Fprintln(ctx.Stdout) 38 | u, err := config.GetURL("/users/" + email + "/tokens") 39 | if err != nil { 40 | return err 41 | } 42 | v := url.Values{} 43 | v.Set("password", password) 44 | b := strings.NewReader(v.Encode()) 45 | request, err := http.NewRequest("POST", u, b) 46 | if err != nil { 47 | return err 48 | } 49 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded") 50 | response, err := tsuruHTTP.UnauthenticatedClient.Do(request) 51 | if err != nil { 52 | return err 53 | } 54 | defer response.Body.Close() 55 | 56 | if response.StatusCode != http.StatusOK { 57 | var body []byte 58 | body, err = io.ReadAll(response.Body) 59 | if err != nil { 60 | return err 61 | } 62 | return fmt.Errorf("failed to authenticate user: %s, %s", response.Status, string(body)) 63 | } 64 | 65 | result, err := io.ReadAll(response.Body) 66 | if err != nil { 67 | return err 68 | } 69 | out := make(map[string]interface{}) 70 | err = json.Unmarshal(result, &out) 71 | if err != nil { 72 | return err 73 | } 74 | fmt.Fprintln(ctx.Stdout, "Successfully logged in!") 75 | err = config.RemoveTokenV2() 76 | if err != nil { 77 | return err 78 | } 79 | return config.WriteTokenV1(out["token"].(string)) 80 | } 81 | -------------------------------------------------------------------------------- /tsuru/auth/oauth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package auth 6 | 7 | import ( 8 | stdContext "context" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "net" 13 | "net/http" 14 | "net/url" 15 | "strings" 16 | "time" 17 | 18 | "github.com/pkg/errors" 19 | "github.com/tsuru/go-tsuruclient/pkg/config" 20 | "github.com/tsuru/tsuru/cmd" 21 | tsuruNet "github.com/tsuru/tsuru/net" 22 | authTypes "github.com/tsuru/tsuru/types/auth" 23 | ) 24 | 25 | func oauthLogin(ctx *cmd.Context, loginInfo *authTypes.SchemeInfo) error { 26 | finish := make(chan bool) 27 | l, err := net.Listen("tcp", port(loginInfo)) 28 | if err != nil { 29 | return err 30 | } 31 | _, port, err := net.SplitHostPort(l.Addr().String()) 32 | if err != nil { 33 | return err 34 | } 35 | redirectURL := fmt.Sprintf("http://localhost:%s", port) 36 | authURL := strings.Replace(loginInfo.Data.AuthorizeURL, "__redirect_url__", redirectURL, 1) 37 | mux := http.NewServeMux() 38 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 39 | defer func() { 40 | finish <- true 41 | }() 42 | var page string 43 | token, handlerErr := convertOAuthToken(r.URL.Query().Get("code"), redirectURL) 44 | if handlerErr != nil { 45 | writeHTMLError(w, handlerErr) 46 | return 47 | } 48 | handlerErr = config.WriteTokenV1(token) 49 | if handlerErr != nil { 50 | writeHTMLError(w, handlerErr) 51 | return 52 | } 53 | handlerErr = config.RemoveTokenV2() 54 | if handlerErr != nil { 55 | writeHTMLError(w, handlerErr) 56 | return 57 | } 58 | page = fmt.Sprintf(callbackPage, successMarkup) 59 | 60 | w.Header().Add("Content-Type", "text/html") 61 | w.Write([]byte(page)) 62 | }) 63 | server := &http.Server{ 64 | Handler: mux, 65 | } 66 | go server.Serve(l) 67 | err = open(authURL) 68 | if err != nil { 69 | fmt.Fprintln(ctx.Stdout, "Failed to start your browser.") 70 | fmt.Fprintf(ctx.Stdout, "Please open the following URL in your browser: %s\n", authURL) 71 | } 72 | <-finish 73 | timedCtx, cancel := stdContext.WithTimeout(stdContext.Background(), 15*time.Second) 74 | defer cancel() 75 | server.Shutdown(timedCtx) 76 | fmt.Fprintln(ctx.Stdout, "Successfully logged in!") 77 | return nil 78 | } 79 | 80 | func convertOAuthToken(code, redirectURL string) (string, error) { 81 | var token string 82 | v := url.Values{} 83 | v.Set("code", code) 84 | v.Set("redirectUrl", redirectURL) 85 | u, err := config.GetURL("/auth/login") 86 | if err != nil { 87 | return token, errors.Wrap(err, "Error in GetURL") 88 | } 89 | resp, err := tsuruNet.Dial15Full300Client.Post(u, "application/x-www-form-urlencoded", strings.NewReader(v.Encode())) 90 | if err != nil { 91 | return token, errors.Wrap(err, "Error during login post") 92 | } 93 | defer resp.Body.Close() 94 | result, err := io.ReadAll(resp.Body) 95 | if err != nil { 96 | return token, errors.Wrap(err, "Error reading body") 97 | } 98 | data := make(map[string]interface{}) 99 | err = json.Unmarshal(result, &data) 100 | if err != nil { 101 | return token, errors.Wrapf(err, "Error parsing response: %s", result) 102 | } 103 | return data["token"].(string), nil 104 | } 105 | -------------------------------------------------------------------------------- /tsuru/auth/oauth_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | package auth 5 | 6 | import ( 7 | "bytes" 8 | "net/http" 9 | "net/http/httptest" 10 | "os" 11 | "time" 12 | 13 | "github.com/tsuru/go-tsuruclient/pkg/config" 14 | "github.com/tsuru/tsuru/cmd" 15 | "github.com/tsuru/tsuru/exec" 16 | "github.com/tsuru/tsuru/fs/fstest" 17 | 18 | "github.com/tsuru/tsuru/types/auth" 19 | "gopkg.in/check.v1" 20 | ) 21 | 22 | type fakeExecutor struct { 23 | DoExecute func(opts exec.ExecuteOptions) error 24 | } 25 | 26 | func (f *fakeExecutor) Execute(opts exec.ExecuteOptions) error { 27 | return f.DoExecute(opts) 28 | } 29 | 30 | func (s *S) TestOAuthLogin(c *check.C) { 31 | 32 | config.SetFileSystem(&fstest.RecordingFs{}) 33 | 34 | execut = &fakeExecutor{ 35 | DoExecute: func(opts exec.ExecuteOptions) error { 36 | 37 | go func() { 38 | time.Sleep(time.Second) 39 | _, err := http.Get("http://localhost:41000") 40 | c.Assert(err, check.IsNil) 41 | }() 42 | 43 | return nil 44 | }, 45 | } 46 | 47 | defer func() { 48 | config.ResetFileSystem() 49 | execut = nil 50 | }() 51 | 52 | fakeTsuruServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 53 | c.Assert(req.URL.Path, check.Equals, "/1.0/auth/login") 54 | rw.Write([]byte(`{"token":"mytoken"}`)) 55 | })) 56 | defer fakeTsuruServer.Close() 57 | 58 | os.Setenv("TSURU_TARGET", fakeTsuruServer.URL) 59 | 60 | context := &cmd.Context{ 61 | Stdout: &bytes.Buffer{}, 62 | } 63 | 64 | err := oauthLogin(context, &auth.SchemeInfo{ 65 | Data: auth.SchemeData{ 66 | Port: "41000", 67 | }, 68 | }) 69 | 70 | c.Assert(err, check.IsNil) 71 | tokenV1, err := config.ReadTokenV1() 72 | c.Assert(err, check.IsNil) 73 | c.Assert(tokenV1, check.Equals, "mytoken") 74 | } 75 | -------------------------------------------------------------------------------- /tsuru/auth/oidc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package auth 6 | 7 | import ( 8 | stdContext "context" 9 | "fmt" 10 | "net" 11 | "net/http" 12 | "time" 13 | 14 | "github.com/tsuru/go-tsuruclient/pkg/config" 15 | "github.com/tsuru/tsuru/cmd" 16 | authTypes "github.com/tsuru/tsuru/types/auth" 17 | "golang.org/x/oauth2" 18 | ) 19 | 20 | func oidcLogin(ctx *cmd.Context, loginInfo *authTypes.SchemeInfo) error { 21 | pkceVerifier := oauth2.GenerateVerifier() 22 | 23 | fmt.Fprintln(ctx.Stderr, "Starting OIDC login") 24 | 25 | l, err := net.Listen("tcp", port(loginInfo)) 26 | if err != nil { 27 | return err 28 | } 29 | _, port, err := net.SplitHostPort(l.Addr().String()) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | oauth2Config := oauth2.Config{ 35 | ClientID: loginInfo.Data.ClientID, 36 | Scopes: loginInfo.Data.Scopes, 37 | RedirectURL: fmt.Sprintf("http://localhost:%s", port), 38 | Endpoint: oauth2.Endpoint{ 39 | AuthURL: loginInfo.Data.AuthURL, 40 | TokenURL: loginInfo.Data.TokenURL, 41 | }, 42 | } 43 | 44 | authURL := oauth2Config.AuthCodeURL("", oauth2.S256ChallengeOption(pkceVerifier)) 45 | 46 | finish := make(chan bool) 47 | 48 | mux := http.NewServeMux() 49 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 50 | 51 | defer func() { 52 | finish <- true 53 | }() 54 | 55 | t, handlerErr := oauth2Config.Exchange(stdContext.Background(), r.URL.Query().Get("code"), oauth2.VerifierOption(pkceVerifier)) 56 | 57 | w.Header().Add("Content-Type", "text/html") 58 | 59 | if handlerErr != nil { 60 | writeHTMLError(w, handlerErr) 61 | return 62 | } 63 | 64 | fmt.Fprintln(ctx.Stderr, "Successfully logged in via OIDC!") 65 | tokenExpiry := time.Since(t.Expiry) * -1 66 | fmt.Fprintf(ctx.Stderr, "The OIDC token will expiry in %s\n", tokenExpiry.Round(time.Second)) 67 | 68 | handlerErr = config.WriteTokenV2(config.TokenV2{ 69 | Scheme: "oidc", 70 | OAuth2Token: t, 71 | OAuth2Config: &oauth2Config, 72 | }) 73 | 74 | if handlerErr != nil { 75 | writeHTMLError(w, handlerErr) 76 | return 77 | } 78 | 79 | // legacy token 80 | handlerErr = config.WriteTokenV1(t.AccessToken) 81 | 82 | if handlerErr != nil { 83 | writeHTMLError(w, handlerErr) 84 | return 85 | } 86 | 87 | fmt.Fprintf(w, callbackPage, successMarkup) 88 | 89 | }) 90 | server := &http.Server{ 91 | Handler: mux, 92 | } 93 | go server.Serve(l) 94 | err = open(authURL) 95 | if err != nil { 96 | fmt.Fprintln(ctx.Stdout, "Failed to start your browser.") 97 | fmt.Fprintf(ctx.Stdout, "Please open the following URL in your browser: %s\n", authURL) 98 | } 99 | <-finish 100 | timedCtx, cancel := stdContext.WithTimeout(stdContext.Background(), 15*time.Second) 101 | defer cancel() 102 | server.Shutdown(timedCtx) 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /tsuru/auth/oidc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | package auth 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "time" 13 | 14 | "github.com/tsuru/go-tsuruclient/pkg/config" 15 | "github.com/tsuru/tsuru/cmd" 16 | "github.com/tsuru/tsuru/exec" 17 | "github.com/tsuru/tsuru/fs/fstest" 18 | "golang.org/x/oauth2" 19 | 20 | "github.com/tsuru/tsuru/types/auth" 21 | "gopkg.in/check.v1" 22 | ) 23 | 24 | func (s *S) TestOIDChLogin(c *check.C) { 25 | 26 | config.SetFileSystem(&fstest.RecordingFs{}) 27 | 28 | execut = &fakeExecutor{ 29 | DoExecute: func(opts exec.ExecuteOptions) error { 30 | 31 | go func() { 32 | time.Sleep(time.Second) 33 | _, err := http.Get("http://localhost:41000/?code=321") 34 | c.Assert(err, check.IsNil) 35 | }() 36 | 37 | return nil 38 | }, 39 | } 40 | 41 | defer func() { 42 | config.ResetFileSystem() 43 | execut = nil 44 | }() 45 | 46 | fakeIDP := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 47 | b, err := io.ReadAll(req.Body) 48 | c.Assert(err, check.IsNil) 49 | body, err := url.ParseQuery(string(b)) 50 | c.Assert(err, check.IsNil) 51 | 52 | c.Assert(body.Get("code"), check.Equals, "321") 53 | 54 | rw.Header().Set("Content-Type", "application/json") 55 | rw.Write([]byte(`{"access_token":"mytoken", "refresh_token": "refreshtoken"}`)) 56 | })) 57 | defer fakeIDP.Close() 58 | 59 | context := &cmd.Context{ 60 | Stdout: &bytes.Buffer{}, 61 | Stderr: &bytes.Buffer{}, 62 | } 63 | 64 | err := oidcLogin(context, &auth.SchemeInfo{ 65 | Data: auth.SchemeData{ 66 | Port: "41000", 67 | TokenURL: fakeIDP.URL, 68 | ClientID: "test-tsuru", 69 | Scopes: []string{"scope1"}, 70 | }, 71 | }) 72 | 73 | c.Assert(err, check.IsNil) 74 | tokenV1, err := config.ReadTokenV1() 75 | c.Assert(err, check.IsNil) 76 | c.Assert(tokenV1, check.Equals, "mytoken") 77 | 78 | tokenV2, err := config.ReadTokenV2() 79 | c.Assert(err, check.IsNil) 80 | c.Assert(tokenV2, check.DeepEquals, &config.TokenV2{ 81 | Scheme: "oidc", 82 | OAuth2Token: &oauth2.Token{ 83 | AccessToken: "mytoken", 84 | RefreshToken: "refreshtoken", 85 | }, 86 | OAuth2Config: &oauth2.Config{ 87 | ClientID: "test-tsuru", 88 | RedirectURL: "http://localhost:41000", 89 | Scopes: []string{"scope1"}, 90 | Endpoint: oauth2.Endpoint{ 91 | TokenURL: fakeIDP.URL, 92 | }, 93 | }, 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /tsuru/auth/open.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !windows && !darwin 6 | // +build !windows,!darwin 7 | 8 | package auth 9 | 10 | import ( 11 | "fmt" 12 | "strings" 13 | 14 | "github.com/tsuru/tsuru/exec" 15 | "golang.org/x/sys/unix" 16 | ) 17 | 18 | func isWSL() bool { 19 | var u unix.Utsname 20 | err := unix.Uname(&u) 21 | if err != nil { 22 | fmt.Println(err) 23 | return false 24 | } 25 | release := strings.ToLower(string(u.Release[:])) 26 | return strings.Contains(release, "microsoft") 27 | } 28 | 29 | func open(url string) error { 30 | 31 | cmd := "xdg-open" 32 | args := []string{url} 33 | 34 | if isWSL() { 35 | cmd = "powershell.exe" 36 | args = []string{"-c", "start", "'" + url + "'"} 37 | } 38 | 39 | opts := exec.ExecuteOptions{ 40 | Cmd: cmd, 41 | Args: args, 42 | } 43 | return executor().Execute(opts) 44 | } 45 | 46 | var execut exec.Executor 47 | 48 | func executor() exec.Executor { 49 | if execut == nil { 50 | execut = exec.OsExecutor{} 51 | } 52 | return execut 53 | } 54 | -------------------------------------------------------------------------------- /tsuru/auth/open_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package auth 6 | 7 | import ( 8 | "github.com/tsuru/tsuru/exec" 9 | ) 10 | 11 | func open(url string) error { 12 | opts := exec.ExecuteOptions{ 13 | Cmd: "open", 14 | Args: []string{url}, 15 | } 16 | return executor().Execute(opts) 17 | } 18 | 19 | var execut exec.Executor 20 | 21 | func executor() exec.Executor { 22 | if execut == nil { 23 | execut = exec.OsExecutor{} 24 | } 25 | return execut 26 | } 27 | -------------------------------------------------------------------------------- /tsuru/auth/open_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package auth 6 | 7 | import ( 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/tsuru/tsuru/exec/exectest" 12 | "gopkg.in/check.v1" 13 | ) 14 | 15 | func (s *S) TestOpen(c *check.C) { 16 | fexec := exectest.FakeExecutor{} 17 | execut = &fexec 18 | defer func() { 19 | execut = nil 20 | }() 21 | url := "http://someurl" 22 | err := open(url) 23 | c.Assert(err, check.IsNil) 24 | 25 | switch runtime.GOOS { 26 | case "linux": 27 | c.Assert(fexec.ExecutedCmd("xdg-open", []string{url}), check.Equals, true) 28 | case "windows": 29 | url = strings.ReplaceAll(url, "&", "^&") 30 | c.Assert(fexec.ExecutedCmd("cmd", []string{"/c", "start", "", url}), check.Equals, true) 31 | default: 32 | c.Assert(fexec.ExecutedCmd("open", []string{url}), check.Equals, true) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tsuru/auth/open_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package auth 6 | 7 | import ( 8 | "strings" 9 | 10 | "github.com/tsuru/tsuru/exec" 11 | ) 12 | 13 | func open(url string) error { 14 | var opts exec.ExecuteOptions 15 | url = strings.Replace(url, "&", "^&", -1) 16 | opts = exec.ExecuteOptions{ 17 | Cmd: "cmd", 18 | Args: []string{"/c", "start", "", url}, 19 | } 20 | return executor().Execute(opts) 21 | } 22 | 23 | var execut exec.Executor 24 | 25 | func executor() exec.Executor { 26 | if execut == nil { 27 | execut = exec.OsExecutor{} 28 | } 29 | return execut 30 | } 31 | -------------------------------------------------------------------------------- /tsuru/auth/suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | package auth 5 | 6 | import ( 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/tsuru/go-tsuruclient/pkg/config" 11 | "gopkg.in/check.v1" 12 | 13 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 14 | ) 15 | 16 | var _ = check.Suite(&S{}) 17 | 18 | func Test(t *testing.T) { check.TestingT(t) } 19 | 20 | type S struct{} 21 | 22 | func (s *S) SetUpTest(c *check.C) { 23 | config.ResetFileSystem() 24 | } 25 | 26 | func setupFakeTransport(rt http.RoundTripper) { 27 | tsuruHTTP.AuthenticatedClient = tsuruHTTP.NewTerminalClient(tsuruHTTP.TerminalClientOptions{ 28 | RoundTripper: rt, 29 | ClientName: "test", 30 | ClientVersion: "0.1.0", 31 | }) 32 | 33 | tsuruHTTP.UnauthenticatedClient = &http.Client{ 34 | Transport: rt, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tsuru/client/apps_type.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "fmt" 9 | 10 | provTypes "github.com/tsuru/tsuru/types/provision" 11 | ) 12 | 13 | func getParamsScaleDownLines(behavior provTypes.BehaviorAutoScaleSpec) []string { 14 | lines := []string{} 15 | 16 | if behavior.ScaleDown.UnitsPolicyValue != nil { 17 | lines = append(lines, fmt.Sprintf("Units: %d", *behavior.ScaleDown.UnitsPolicyValue)) 18 | } 19 | if behavior.ScaleDown.PercentagePolicyValue != nil { 20 | lines = append(lines, fmt.Sprintf("Percentage: %d%%", *behavior.ScaleDown.PercentagePolicyValue)) 21 | } 22 | if behavior.ScaleDown.StabilizationWindow != nil { 23 | lines = append(lines, fmt.Sprintf("Stabilization window: %ds", *behavior.ScaleDown.StabilizationWindow)) 24 | } 25 | return lines 26 | } 27 | -------------------------------------------------------------------------------- /tsuru/client/archiver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "archive/tar" 9 | "compress/gzip" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "io/fs" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | 18 | gitignore "github.com/sabhiram/go-gitignore" 19 | ) 20 | 21 | var ErrMissingFilesToArchive = errors.New("missing files to archive") 22 | 23 | type ArchiveOptions struct { 24 | CompressionLevel *int // defaults to default compression "-1" 25 | IgnoreFiles []string // default to none 26 | Stderr io.Writer // defaults to io.Discard 27 | } 28 | 29 | func DefaultArchiveOptions(w io.Writer) ArchiveOptions { 30 | return ArchiveOptions{ 31 | CompressionLevel: func(lvl int) *int { return &lvl }(gzip.BestCompression), 32 | IgnoreFiles: []string{".tsuruignore"}, 33 | Stderr: w, 34 | } 35 | } 36 | 37 | func Archive(dst io.Writer, filesOnly bool, paths []string, opts ArchiveOptions) error { 38 | if dst == nil { 39 | return fmt.Errorf("destination cannot be nil") 40 | } 41 | 42 | if len(paths) == 0 { 43 | return fmt.Errorf("paths cannot be empty") 44 | } 45 | 46 | if opts.Stderr == nil { 47 | opts.Stderr = io.Discard 48 | } 49 | 50 | var ignoreLines []string 51 | for _, ignoreFile := range opts.IgnoreFiles { 52 | data, err := os.ReadFile(ignoreFile) 53 | if errors.Is(err, os.ErrNotExist) { 54 | continue 55 | } 56 | 57 | if err != nil { 58 | return fmt.Errorf("failed to read ignore file %q: %w", ignoreFile, err) 59 | } 60 | 61 | fmt.Fprintf(opts.Stderr, "Using pattern(s) from %q to include/exclude files...\n", ignoreFile) 62 | 63 | ignoreLines = append(ignoreLines, strings.Split(string(data), "\n")...) 64 | } 65 | 66 | ignore, err := gitignore.CompileIgnoreLines(ignoreLines...) 67 | if err != nil { 68 | return fmt.Errorf("failed to compile all ignore patterns: %w", err) 69 | } 70 | 71 | if opts.CompressionLevel == nil { 72 | opts.CompressionLevel = func(n int) *int { return &n }(gzip.DefaultCompression) 73 | } 74 | 75 | zw, err := gzip.NewWriterLevel(dst, *opts.CompressionLevel) 76 | if err != nil { 77 | return err 78 | } 79 | defer zw.Close() 80 | 81 | tw := tar.NewWriter(zw) 82 | defer tw.Close() 83 | 84 | a := &archiver{ 85 | ignore: *ignore, 86 | stderr: opts.Stderr, 87 | files: map[string]struct{}{}, 88 | } 89 | 90 | return a.archive(tw, filesOnly, paths) 91 | } 92 | 93 | type archiver struct { 94 | ignore gitignore.GitIgnore 95 | stderr io.Writer 96 | files map[string]struct{} 97 | } 98 | 99 | func (a *archiver) archive(tw *tar.Writer, filesOnly bool, paths []string) error { 100 | workingDir, err := os.Getwd() 101 | if err != nil { 102 | return fmt.Errorf("failed to get the current directory: %w", err) 103 | } 104 | 105 | var added int 106 | 107 | for _, path := range paths { 108 | abs, err := filepath.Abs(path) 109 | if err != nil { 110 | return fmt.Errorf("failed to get the absolute filename of %q: %w", path, err) 111 | } 112 | 113 | if !strings.HasPrefix((abs + string(os.PathSeparator)), (workingDir + string(os.PathSeparator))) { 114 | fmt.Fprintf(a.stderr, "WARNING: skipping file %q since you cannot add files outside the current directory\n", path) 115 | continue 116 | } 117 | 118 | fi, err := os.Lstat(path) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | var n int 124 | 125 | var changeDir string 126 | if fi.IsDir() { 127 | // NOTE(nettoclaudio): when either user passes a single directory or files 128 | // only flag is turned on, we should consider it as the root directory for 129 | // backward-compatibility. 130 | subdirFilesOnly := filesOnly 131 | if filesOnly || len(paths) == 1 { 132 | changeDir, path = abs, "." 133 | subdirFilesOnly = false 134 | } 135 | 136 | n, err = a.addDir(tw, subdirFilesOnly, path, changeDir) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | added += n 142 | continue 143 | } 144 | 145 | n, err = a.addFile(tw, filesOnly, path, fi) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | added += n 151 | } 152 | 153 | if added == 0 { 154 | return ErrMissingFilesToArchive 155 | } 156 | 157 | return nil 158 | } 159 | 160 | func (a *archiver) addFile(tw *tar.Writer, filesOnly bool, filename string, fi os.FileInfo) (int, error) { 161 | isDir, isRegular, isSymlink := fi.IsDir(), fi.Mode().IsRegular(), fi.Mode()&os.ModeSymlink == os.ModeSymlink 162 | 163 | if !isDir && !isRegular && !isSymlink { // neither dir, regular nor symlink 164 | fmt.Fprintf(a.stderr, "WARNING: Skipping file %q due to unsupported file type.\n", filename) 165 | return 0, nil 166 | } 167 | 168 | if isDir && filesOnly { // there's no need to create dirs in files only 169 | return 0, nil 170 | } 171 | 172 | if a.ignore.MatchesPath(filename) { 173 | fmt.Fprintf(a.stderr, "File %q matches with some pattern provided in the ignore file... skipping it.\n", filename) 174 | return 0, nil 175 | } 176 | 177 | var linkname string 178 | if isSymlink { 179 | target, err := os.Readlink(filename) 180 | if err != nil { 181 | return 0, err 182 | } 183 | 184 | linkname = target 185 | } 186 | 187 | h, err := tar.FileInfoHeader(fi, linkname) 188 | if err != nil { 189 | return 0, err 190 | } 191 | 192 | if !filesOnly { // should preserve the directory tree 193 | h.Name = filename 194 | } 195 | 196 | if _, found := a.files[h.Name]; found { 197 | fmt.Fprintf(a.stderr, "Skipping file %q as it already exists in the current directory.\n", filename) 198 | return 0, nil 199 | } 200 | 201 | a.files[h.Name] = struct{}{} 202 | 203 | if strings.TrimRight(h.Name, string(os.PathSeparator)) == "." { // skipping root dir 204 | return 0, nil 205 | } 206 | 207 | if err = tw.WriteHeader(h); err != nil { 208 | return 0, err 209 | } 210 | 211 | if isDir || isSymlink { // there's no data to copy from dir or symlink 212 | return 1, nil 213 | } 214 | 215 | f, err := os.Open(filename) 216 | if err != nil { 217 | return 0, err 218 | } 219 | defer f.Close() 220 | 221 | written, err := io.CopyN(tw, f, h.Size) 222 | if err != nil { 223 | return 0, err 224 | } 225 | 226 | if written < h.Size { 227 | return 0, io.ErrShortWrite 228 | } 229 | 230 | return 1, nil 231 | } 232 | 233 | func (a *archiver) addDir(tw *tar.Writer, filesOnly bool, path, changeDir string) (int, error) { 234 | if changeDir != "" { 235 | cwd, err := os.Getwd() 236 | if err != nil { 237 | return 0, err 238 | } 239 | 240 | defer os.Chdir(cwd) 241 | 242 | if err = os.Chdir(changeDir); err != nil { 243 | return 0, err 244 | } 245 | } 246 | 247 | var added int 248 | return added, filepath.WalkDir(path, fs.WalkDirFunc(func(path string, dentry fs.DirEntry, err error) error { 249 | if err != nil { // fail fast 250 | return err 251 | } 252 | 253 | fi, err := dentry.Info() 254 | if err != nil { 255 | return err 256 | } 257 | 258 | var n int 259 | n, err = a.addFile(tw, filesOnly, path, fi) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | added += n 265 | 266 | return nil 267 | })) 268 | } 269 | -------------------------------------------------------------------------------- /tsuru/client/archiver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "archive/tar" 9 | "bytes" 10 | "compress/gzip" 11 | "errors" 12 | "io" 13 | "testing" 14 | 15 | check "gopkg.in/check.v1" 16 | ) 17 | 18 | func extractFiles(t *testing.T, c *check.C, r io.Reader) (m []miniFile) { 19 | t.Helper() 20 | 21 | gzr, err := gzip.NewReader(r) 22 | c.Assert(err, check.IsNil) 23 | 24 | tr := tar.NewReader(gzr) 25 | 26 | for { 27 | h, err := tr.Next() 28 | if errors.Is(err, io.EOF) { 29 | break 30 | } 31 | 32 | c.Assert(err, check.IsNil) 33 | 34 | var data []byte 35 | 36 | if h.Typeflag == tar.TypeReg { 37 | var b bytes.Buffer 38 | written, err := io.CopyN(&b, tr, h.Size) 39 | c.Assert(err, check.IsNil) 40 | c.Assert(written, check.Equals, h.Size) 41 | data = b.Bytes() 42 | } 43 | 44 | m = append(m, miniFile{ 45 | Name: h.Name, 46 | Linkname: h.Linkname, 47 | Type: h.Typeflag, 48 | Data: data, 49 | }) 50 | } 51 | 52 | return m 53 | } 54 | 55 | type miniFile struct { 56 | Name string 57 | Linkname string 58 | Type byte 59 | Data []byte 60 | } 61 | -------------------------------------------------------------------------------- /tsuru/client/build_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "bytes" 9 | "io" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/tsuru/tsuru/cmd" 16 | "github.com/tsuru/tsuru/cmd/cmdtest" 17 | "gopkg.in/check.v1" 18 | ) 19 | 20 | func (s *S) TestBuildInfo(c *check.C) { 21 | var cmd AppBuild 22 | c.Assert(cmd.Info(), check.NotNil) 23 | } 24 | 25 | func (s *S) TestBuildRun(c *check.C) { 26 | calledTimes := 0 27 | var buf bytes.Buffer 28 | err := Archive(&buf, false, []string{"testdata", ".."}, DefaultArchiveOptions(io.Discard)) 29 | c.Assert(err, check.IsNil) 30 | trans := cmdtest.ConditionalTransport{ 31 | Transport: cmdtest.Transport{Message: "\nOK\n", Status: http.StatusOK}, 32 | CondFunc: func(req *http.Request) bool { 33 | calledTimes++ 34 | if req.Body != nil { 35 | defer req.Body.Close() 36 | } 37 | if calledTimes == 1 { 38 | return req.Method == "GET" && strings.HasSuffix(req.URL.Path, "/apps/myapp") 39 | } 40 | file, _, transErr := req.FormFile("file") 41 | c.Assert(transErr, check.IsNil) 42 | content, transErr := io.ReadAll(file) 43 | c.Assert(transErr, check.IsNil) 44 | c.Assert(content, check.DeepEquals, buf.Bytes()) 45 | c.Assert(req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") 46 | return req.Method == "POST" && strings.HasSuffix(req.URL.Path, "/apps/myapp/build") 47 | }, 48 | } 49 | s.setupFakeTransport(&trans) 50 | var stdout, stderr bytes.Buffer 51 | context := cmd.Context{ 52 | Stdout: &stdout, 53 | Stderr: &stderr, 54 | Args: []string{"testdata", ".."}, 55 | } 56 | command := AppBuild{} 57 | command.Flags().Parse(true, []string{"-a", "myapp", "-t", "mytag"}) 58 | err = command.Run(&context) 59 | c.Assert(err, check.IsNil) 60 | c.Assert(calledTimes, check.Equals, 2) 61 | } 62 | 63 | func (s *S) TestBuildFail(c *check.C) { 64 | var buf bytes.Buffer 65 | err := Archive(&buf, false, []string{"testdata", ".."}, DefaultArchiveOptions(io.Discard)) 66 | c.Assert(err, check.IsNil) 67 | trans := cmdtest.ConditionalTransport{ 68 | Transport: cmdtest.Transport{Message: "Failed", Status: http.StatusOK}, 69 | CondFunc: func(req *http.Request) bool { 70 | if req.Body != nil { 71 | defer req.Body.Close() 72 | } 73 | if req.Method == "GET" { 74 | return strings.HasSuffix(req.URL.Path, "/apps/myapp") 75 | } 76 | file, _, transErr := req.FormFile("file") 77 | c.Assert(transErr, check.IsNil) 78 | content, transErr := io.ReadAll(file) 79 | c.Assert(transErr, check.IsNil) 80 | c.Assert(content, check.DeepEquals, buf.Bytes()) 81 | c.Assert(req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") 82 | return req.Method == "POST" && strings.HasSuffix(req.URL.Path, "/apps/myapp/build") 83 | }, 84 | } 85 | s.setupFakeTransport(&trans) 86 | var stdout, stderr bytes.Buffer 87 | context := cmd.Context{ 88 | Stdout: &stdout, 89 | Stderr: &stderr, 90 | Args: []string{"testdata", ".."}, 91 | } 92 | command := AppBuild{} 93 | command.Flags().Parse(true, []string{"-a", "myapp", "-t", "mytag"}) 94 | err = command.Run(&context) 95 | c.Assert(err, check.Equals, cmd.ErrAbortCommand) 96 | } 97 | 98 | func (s *S) TestBuildRunWithoutArgs(c *check.C) { 99 | var stdout, stderr bytes.Buffer 100 | ctx := cmd.Context{ 101 | Stdout: &stdout, 102 | Stderr: &stderr, 103 | Args: []string{}, 104 | } 105 | trans := cmdtest.Transport{Message: "OK\n", Status: http.StatusOK} 106 | s.setupFakeTransport(&trans) 107 | command := AppBuild{} 108 | command.Flags().Parse(true, []string{"-a", "myapp", "-t", "mytag"}) 109 | err := command.Run(&ctx) 110 | c.Assert(err, check.NotNil) 111 | c.Assert(err.Error(), check.Equals, "you should provide at least one file to build the image") 112 | } 113 | 114 | func (s *S) TestBuildRunWithoutTag(c *check.C) { 115 | var stdout, stderr bytes.Buffer 116 | ctx := cmd.Context{ 117 | Stdout: &stdout, 118 | Stderr: &stderr, 119 | Args: []string{"testdata", "..", "-a", "myapp"}, 120 | } 121 | trans := cmdtest.Transport{Message: "OK\n", Status: http.StatusOK} 122 | s.setupFakeTransport(&trans) 123 | command := AppBuild{} 124 | command.Flags().Parse(true, []string{"-a", "myapp"}) 125 | err := command.Run(&ctx) 126 | c.Assert(err, check.NotNil) 127 | c.Assert(err.Error(), check.Equals, "you should provide one tag to build the image") 128 | } 129 | 130 | func (s *S) TestGuessingContainerFile(c *check.C) { 131 | cases := []struct { 132 | files []string 133 | app string 134 | expected func(d string) string 135 | expectedError string 136 | }{ 137 | { 138 | expectedError: "container file not found", 139 | }, 140 | { 141 | app: "my-app", 142 | files: []string{"Containerfile"}, 143 | expected: func(root string) string { return filepath.Join(root, "Containerfile") }, 144 | }, 145 | { 146 | app: "my-app", 147 | files: []string{"Containerfile", "Dockerfile"}, 148 | expected: func(root string) string { return filepath.Join(root, "Dockerfile") }, 149 | }, 150 | { 151 | app: "my-app", 152 | files: []string{"Containerfile", "Dockerfile", "Containerfile.tsuru"}, 153 | expected: func(root string) string { return filepath.Join(root, "Containerfile.tsuru") }, 154 | }, 155 | { 156 | app: "my-app", 157 | files: []string{"Containerfile", "Dockerfile", "Containerfile.tsuru", "Dockerfile.tsuru"}, 158 | expected: func(root string) string { return filepath.Join(root, "Dockerfile.tsuru") }, 159 | }, 160 | { 161 | app: "my-app", 162 | files: []string{"Containerfile", "Dockerfile", "Containerfile.tsuru", "Dockerfile.tsuru", "Containerfile.my-app"}, 163 | expected: func(root string) string { return filepath.Join(root, "Containerfile.my-app") }, 164 | }, 165 | { 166 | app: "my-app", 167 | files: []string{"Containerfile", "Dockerfile", "Containerfile.tsuru", "Dockerfile.tsuru", "Containerfile.my-app", "Dockerfile.my-app"}, 168 | expected: func(root string) string { return filepath.Join(root, "Dockerfile.my-app") }, 169 | }, 170 | } 171 | 172 | for _, tt := range cases { 173 | dir := c.MkDir() 174 | 175 | for _, name := range tt.files { 176 | f, err := os.Create(filepath.Join(dir, name)) 177 | c.Check(err, check.IsNil) 178 | c.Check(f.Close(), check.IsNil) 179 | } 180 | 181 | got, err := guessingContainerFile(tt.app, dir) 182 | if tt.expectedError != "" { 183 | c.Check(err, check.ErrorMatches, tt.expectedError) 184 | } else { 185 | c.Check(err, check.IsNil) 186 | c.Check(got, check.DeepEquals, tt.expected(dir)) 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /tsuru/client/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | tsuru is the command line utility used by application developers, 7 | that will allow users to create, list, bind and manage apps. 8 | 9 | See the tsuru-client documentation for a full reference: http://tsuru-client.readthedocs.org. 10 | */ 11 | package client 12 | -------------------------------------------------------------------------------- /tsuru/client/executor.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !windows 6 | // +build !windows 7 | 8 | package client 9 | 10 | import ( 11 | "github.com/tsuru/tsuru/exec" 12 | ) 13 | 14 | var Execut exec.Executor 15 | 16 | func Executor() exec.Executor { 17 | if Execut == nil { 18 | Execut = exec.OsExecutor{} 19 | } 20 | return Execut 21 | } 22 | -------------------------------------------------------------------------------- /tsuru/client/executor_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !windows 6 | // +build !windows 7 | 8 | package client 9 | 10 | import ( 11 | "github.com/tsuru/tsuru/exec" 12 | "github.com/tsuru/tsuru/exec/exectest" 13 | "gopkg.in/check.v1" 14 | ) 15 | 16 | func (s *S) TestExecutor(c *check.C) { 17 | Execut = &exectest.FakeExecutor{} 18 | c.Assert(Executor(), check.DeepEquals, Execut) 19 | Execut = nil 20 | c.Assert(Executor(), check.DeepEquals, exec.OsExecutor{}) 21 | } 22 | -------------------------------------------------------------------------------- /tsuru/client/executor_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | osexec "os/exec" 9 | 10 | "github.com/tsuru/tsuru/exec" 11 | ) 12 | 13 | var Execut exec.Executor 14 | 15 | func Executor() exec.Executor { 16 | if Execut == nil { 17 | Execut = windowsCmdExecutor{} 18 | } 19 | return Execut 20 | } 21 | 22 | type windowsCmdExecutor struct{} 23 | 24 | func (windowsCmdExecutor) Execute(opts exec.ExecuteOptions) error { 25 | args := append([]string{ 26 | "/c", 27 | opts.Cmd, 28 | }, opts.Args...) 29 | c := osexec.Command("cmd", args...) 30 | c.Stdin = opts.Stdin 31 | c.Stdout = opts.Stdout 32 | c.Stderr = opts.Stderr 33 | c.Env = opts.Envs 34 | c.Dir = opts.Dir 35 | return c.Run() 36 | } 37 | -------------------------------------------------------------------------------- /tsuru/client/flags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "github.com/tsuru/gnuflag" 9 | ) 10 | 11 | func mergeFlagSet(fs1, fs2 *gnuflag.FlagSet) *gnuflag.FlagSet { 12 | fs2.VisitAll(func(flag *gnuflag.Flag) { 13 | fs1.Var(flag.Value, flag.Name, flag.Usage) 14 | }) 15 | return fs1 16 | } 17 | -------------------------------------------------------------------------------- /tsuru/client/flags_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "github.com/tsuru/gnuflag" 9 | check "gopkg.in/check.v1" 10 | ) 11 | 12 | func (s *S) TestMergeFlagSet(c *check.C) { 13 | var x, y bool 14 | fs1 := gnuflag.NewFlagSet("x", gnuflag.ExitOnError) 15 | fs1.BoolVar(&x, "x", false, "Something") 16 | fs2 := gnuflag.NewFlagSet("y", gnuflag.ExitOnError) 17 | fs2.BoolVar(&y, "y", false, "Something") 18 | ret := mergeFlagSet(fs1, fs2) 19 | c.Assert(ret, check.Equals, fs1) 20 | fs1.Parse(true, []string{"-x", "-y"}) 21 | c.Assert(x, check.Equals, true) 22 | c.Assert(y, check.Equals, true) 23 | } 24 | -------------------------------------------------------------------------------- /tsuru/client/init.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | tsuruErrors "github.com/tsuru/tsuru/errors" 10 | 11 | "github.com/tsuru/tsuru/cmd" 12 | ) 13 | 14 | type Init struct{} 15 | 16 | func (i *Init) Info() *cmd.Info { 17 | return &cmd.Info{ 18 | Name: "init", 19 | Usage: "init", 20 | Desc: ` 21 | Creates a standard example of .tsuruignore , tsuru.yaml and Procfile 22 | on the current project directory. 23 | 24 | "Procfile" describes the components required to run an application. 25 | It is the way to tell tsuru how to run your applications; 26 | 27 | ".tsuruignore" describes to tsuru what it should not add into your 28 | deploy process, via "tsuru app deploy" command. You can use 29 | ".tsuruignore" to avoid sending files that were committed, 30 | but aren't necessary for running the app, like tests and 31 | documentation; 32 | 33 | "tsuru.yaml" describes certain aspects of your app, like information 34 | about deployment hooks and deployment time health checks.`, 35 | } 36 | } 37 | 38 | func (i *Init) Run(context *cmd.Context) (err error) { 39 | err = createInitFiles() 40 | if err != nil { 41 | return 42 | } 43 | const msg = ` 44 | Initialized Tsuru sample files: "Procfile", ".tsuruignore" and "tsuru.yaml", 45 | for more info please refer to "tsuru init -h" or the docs at docs.tsuru.io` 46 | _, err = context.Stdout.Write([]byte(msg)) 47 | if err != nil { 48 | return 49 | } 50 | err = writeTsuruYaml() 51 | if err != nil { 52 | return 53 | } 54 | err = writeProcfile() 55 | if err != nil { 56 | return 57 | } 58 | return copyGitIgnore() 59 | } 60 | 61 | func createInitFiles() (err error) { 62 | wd, err := os.Getwd() 63 | if err != nil { 64 | return 65 | } 66 | fi, err := os.Open(wd) 67 | if err != nil { 68 | return 69 | } 70 | defer func() { 71 | if errClose := fi.Close(); errClose != nil { 72 | err = tsuruErrors.NewMultiError(err, errClose) 73 | } 74 | }() 75 | dirFiles, err := fi.Readdir(0) 76 | if err != nil { 77 | return 78 | } 79 | initFiles := map[string]string{ 80 | ".tsuruignore": "", 81 | "Procfile": "", 82 | "tsuru.yaml": "", 83 | } 84 | for _, f := range dirFiles { 85 | delete(initFiles, f.Name()) 86 | } 87 | for f := range initFiles { 88 | _, err = os.Create(f) 89 | if err != nil { 90 | return 91 | } 92 | } 93 | return 94 | } 95 | 96 | func copyGitIgnore() (err error) { 97 | in, err := os.Open(".gitignore") 98 | if err != nil { 99 | dotGit := []byte(".git\n.gitignore\n") 100 | return os.WriteFile(".tsuruignore", dotGit, 0644) 101 | } 102 | defer func() { 103 | if errClose := in.Close(); errClose != nil { 104 | err = tsuruErrors.NewMultiError(err, errClose) 105 | } 106 | }() 107 | out, err := os.OpenFile(".tsuruignore", os.O_APPEND|os.O_WRONLY, os.ModeAppend) 108 | if err != nil { 109 | return 110 | } 111 | defer func() { 112 | if errClose := out.Close(); errClose != nil { 113 | err = tsuruErrors.NewMultiError(err, errClose) 114 | } 115 | }() 116 | _, err = io.Copy(out, in) 117 | if err != nil { 118 | return 119 | } 120 | _, err = out.WriteString("\n.git\n.gitignore\n") 121 | return 122 | } 123 | 124 | func writeTsuruYaml() error { 125 | return os.WriteFile("tsuru.yaml", nil, 0644) 126 | } 127 | 128 | func writeProcfile() (err error) { 129 | wd, err := os.Getwd() 130 | if err != nil { 131 | return 132 | } 133 | projectName := filepath.Base(wd) 134 | procfile := fmt.Sprintf("web: %s", projectName) 135 | return os.WriteFile("Procfile", []byte(procfile), 0644) 136 | } 137 | -------------------------------------------------------------------------------- /tsuru/client/init_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/tsuru/tsuru/cmd" 10 | 11 | check "gopkg.in/check.v1" 12 | ) 13 | 14 | func (s *S) TestInitCreateInitFiles(c *check.C) { 15 | wd, err := os.Getwd() 16 | c.Assert(err, check.IsNil) 17 | defer os.Chdir(wd) 18 | deploy3 := filepath.Join("testdata", "deploy3") 19 | err = os.Chdir(filepath.Join(wd, deploy3)) 20 | c.Assert(err, check.IsNil) 21 | err = os.Mkdir("fakeDir3", os.ModePerm) 22 | defer os.RemoveAll(filepath.Join(wd, deploy3, "fakeDir3")) 23 | c.Assert(err, check.IsNil) 24 | err = os.Chdir("fakeDir3") 25 | c.Assert(err, check.IsNil) 26 | err = createInitFiles() 27 | c.Assert(err, check.IsNil) 28 | tpath, err := os.Open(filepath.Join(wd, deploy3, "fakeDir3")) 29 | c.Assert(err, check.IsNil) 30 | defer tpath.Close() 31 | content, err := tpath.Readdir(0) 32 | c.Assert(err, check.IsNil) 33 | var createdFiles []string 34 | for _, c := range content { 35 | if (c.Name() == ".tsuruignore") || (c.Name() == "Procfile") || (c.Name() == "tsuru.yaml") { 36 | createdFiles = append(createdFiles, c.Name()) 37 | } 38 | } 39 | if len(createdFiles) != 3 { 40 | err = errors.New("Tsuru init failed to create a file") 41 | } 42 | c.Assert(err, check.IsNil) 43 | } 44 | 45 | func (s *S) TestInitInfo(c *check.C) { 46 | c.Assert((&Init{}).Info(), check.NotNil) 47 | } 48 | 49 | func (s *S) TestCopyGitIgnoreWithGitIgnore(c *check.C) { 50 | expected := "vendor/\n.git\n.gitignore\n" 51 | wd, err := os.Getwd() 52 | c.Assert(err, check.IsNil) 53 | defer os.Chdir(wd) 54 | testPath := filepath.Join("testdata", "deploy3") 55 | err = os.Chdir(filepath.Join(wd, testPath)) 56 | c.Assert(err, check.IsNil) 57 | err = copyGitIgnore() 58 | c.Assert(err, check.IsNil) 59 | defer os.WriteFile(".tsuruignore", []byte(""), 0644) 60 | tsuruignore, err := os.ReadFile(".tsuruignore") 61 | c.Assert(err, check.IsNil) 62 | c.Assert(string(tsuruignore), check.Equals, expected) 63 | } 64 | 65 | func (s *S) TestCopyGitIgnoreWithoutGitIgnore(c *check.C) { 66 | expected := ".git\n.gitignore\n" 67 | wd, err := os.Getwd() 68 | c.Assert(err, check.IsNil) 69 | defer os.Chdir(wd) 70 | tmpDir := os.TempDir() 71 | err = os.Chdir(tmpDir) 72 | c.Assert(err, check.IsNil) 73 | err = copyGitIgnore() 74 | c.Assert(err, check.IsNil) 75 | defer os.Remove(filepath.Join(tmpDir, ".tsuruignore")) 76 | tsuruignore, err := os.ReadFile(".tsuruignore") 77 | c.Assert(err, check.IsNil) 78 | c.Assert(string(tsuruignore), check.Equals, expected) 79 | } 80 | 81 | func (s *S) TestInitRun(c *check.C) { 82 | wd, err := os.Getwd() 83 | c.Assert(err, check.IsNil) 84 | fakeRunDir := filepath.Join(wd, "testdata", "deploy3", "fakeRun") 85 | err = os.Mkdir(fakeRunDir, os.ModePerm) 86 | c.Assert(err, check.IsNil) 87 | defer os.RemoveAll(fakeRunDir) 88 | defer os.Chdir(wd) 89 | err = os.Chdir(fakeRunDir) 90 | c.Assert(err, check.IsNil) 91 | var stdout bytes.Buffer 92 | context := cmd.Context{Stdout: &stdout} 93 | cmd := Init{} 94 | err = cmd.Run(&context) 95 | c.Assert(err, check.IsNil) 96 | fkRun, err := os.Open(fakeRunDir) 97 | c.Assert(err, check.IsNil) 98 | defer fkRun.Close() 99 | content, err := fkRun.Readdir(0) 100 | c.Assert(err, check.IsNil) 101 | c.Assert(len(content), check.Equals, 3) 102 | } 103 | -------------------------------------------------------------------------------- /tsuru/client/job_or_app.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "errors" 9 | 10 | "github.com/tsuru/gnuflag" 11 | ) 12 | 13 | type JobOrApp struct { 14 | Type string 15 | val string 16 | appProcess string 17 | fs *gnuflag.FlagSet 18 | } 19 | 20 | func (c *JobOrApp) validate() error { 21 | appName := c.fs.Lookup("app").Value.String() 22 | jobName := c.fs.Lookup("job").Value.String() 23 | var processName string 24 | 25 | if flag := c.fs.Lookup("process"); flag != nil { 26 | processName = flag.Value.String() 27 | } 28 | 29 | if appName == "" && jobName == "" { 30 | return errors.New("job name or app name is required") 31 | } 32 | if appName != "" && jobName != "" { 33 | return errors.New("please use only one of the -a/--app and -j/--job flags") 34 | } 35 | if processName != "" && jobName != "" { 36 | return errors.New("please specify process just for an app") 37 | } 38 | if appName != "" { 39 | c.Type = "app" 40 | c.val = appName 41 | c.appProcess = processName 42 | return nil 43 | } 44 | c.Type = "job" 45 | c.val = jobName 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /tsuru/client/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "strings" 13 | "time" 14 | 15 | "github.com/tsuru/gnuflag" 16 | "github.com/tsuru/go-tsuruclient/pkg/config" 17 | tsuruClientApp "github.com/tsuru/tsuru-client/tsuru/app" 18 | "github.com/tsuru/tsuru-client/tsuru/formatter" 19 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 20 | "github.com/tsuru/tsuru/cmd" 21 | ) 22 | 23 | type AppLog struct { 24 | tsuruClientApp.AppNameMixIn 25 | fs *gnuflag.FlagSet 26 | source string 27 | unit string 28 | lines int 29 | follow bool 30 | noDate bool 31 | noSource bool 32 | } 33 | 34 | func (c *AppLog) Info() *cmd.Info { 35 | return &cmd.Info{ 36 | Name: "app-log", 37 | Usage: "app log [appname] [-l/--lines numberOfLines] [-s/--source source] [-u/--unit unit] [-f/--follow]", 38 | Desc: `Shows log entries for an application. These logs include everything the 39 | application send to stdout and stderr, alongside with logs from tsuru server 40 | (deployments, restarts, etc.) 41 | 42 | The [[--lines]] flag is optional and by default its value is 10. 43 | 44 | The [[--source]] flag is optional and allows filtering logs by log source 45 | (e.g. application, tsuru api). 46 | 47 | The [[--unit]] flag is optional and allows filtering by unit. It's useful if 48 | your application has multiple units and you want logs from a single one. 49 | 50 | The [[--follow]] flag is optional and makes the command wait for additional 51 | log output 52 | 53 | The [[--no-date]] flag is optional and makes the log output without date. 54 | 55 | The [[--no-source]] flag is optional and makes the log output without source 56 | information, useful to very dense logs. 57 | `, 58 | MinArgs: 0, 59 | } 60 | } 61 | 62 | type logFormatter struct { 63 | noDate bool 64 | noSource bool 65 | } 66 | 67 | func (f logFormatter) Format(out io.Writer, dec *json.Decoder) error { 68 | var logs []log 69 | err := dec.Decode(&logs) 70 | if err != nil { 71 | if err == io.EOF { 72 | return err 73 | } 74 | buffered := dec.Buffered() 75 | bufferedData, _ := io.ReadAll(buffered) 76 | return fmt.Errorf("unable to parse json: %v: %q", err, string(bufferedData)) 77 | } 78 | for _, l := range logs { 79 | prefix := f.prefix(l) 80 | 81 | if prefix == "" { 82 | fmt.Fprintf(out, "%s\n", l.Message) 83 | } else { 84 | fmt.Fprintf(out, "%s %s\n", cmd.Colorfy(prefix, "blue", "", ""), l.Message) 85 | } 86 | } 87 | return nil 88 | } 89 | 90 | func (f logFormatter) prefix(l log) string { 91 | parts := make([]string, 0, 2) 92 | if !f.noDate { 93 | parts = append(parts, formatter.Local(l.Date).Format("2006-01-02 15:04:05 -0700")) 94 | } 95 | if !f.noSource { 96 | if l.Unit != "" && l.Source != "" { 97 | parts = append(parts, fmt.Sprintf("[%s][%s]", l.Source, l.Unit)) 98 | } else if l.Unit != "" { 99 | parts = append(parts, fmt.Sprintf("[%s]", l.Unit)) 100 | } else { 101 | parts = append(parts, fmt.Sprintf("[%s]", l.Source)) 102 | } 103 | } 104 | prefix := strings.Join(parts, " ") 105 | if prefix != "" { 106 | prefix = prefix + ":" 107 | } 108 | return prefix 109 | } 110 | 111 | type log struct { 112 | Date time.Time 113 | Message string 114 | Source string 115 | Unit string 116 | } 117 | 118 | func (c *AppLog) Run(context *cmd.Context) error { 119 | context.RawOutput() 120 | appName, err := c.AppNameByArgsAndFlag(context.Args) 121 | if err != nil { 122 | return err 123 | } 124 | url, err := config.GetURL(fmt.Sprintf("/apps/%s/log?lines=%d", appName, c.lines)) 125 | if err != nil { 126 | return err 127 | } 128 | if c.source != "" { 129 | url = fmt.Sprintf("%s&source=%s", url, c.source) 130 | } 131 | if c.unit != "" { 132 | url = fmt.Sprintf("%s&unit=%s", url, c.unit) 133 | } 134 | if c.follow { 135 | url += "&follow=1" 136 | } 137 | request, err := http.NewRequest("GET", url, nil) 138 | if err != nil { 139 | return err 140 | } 141 | response, err := tsuruHTTP.AuthenticatedClient.Do(request) 142 | if err != nil { 143 | return err 144 | } 145 | if response.StatusCode == http.StatusNoContent { 146 | return nil 147 | } 148 | defer response.Body.Close() 149 | formatter := logFormatter{ 150 | noDate: c.noDate, 151 | noSource: c.noSource, 152 | } 153 | dec := json.NewDecoder(response.Body) 154 | for { 155 | err = formatter.Format(context.Stdout, dec) 156 | if err != nil { 157 | if err != io.EOF { 158 | fmt.Fprintf(context.Stdout, "Error: %v", err) 159 | } 160 | break 161 | } 162 | } 163 | return nil 164 | } 165 | 166 | func (c *AppLog) Flags() *gnuflag.FlagSet { 167 | if c.fs == nil { 168 | c.fs = c.AppNameMixIn.Flags() 169 | c.fs.IntVar(&c.lines, "lines", 10, "The number of log lines to display") 170 | c.fs.IntVar(&c.lines, "l", 10, "The number of log lines to display") 171 | c.fs.StringVar(&c.source, "source", "", "The log from the given source") 172 | c.fs.StringVar(&c.source, "s", "", "The log from the given source") 173 | c.fs.StringVar(&c.unit, "unit", "", "The log from the given unit") 174 | c.fs.StringVar(&c.unit, "u", "", "The log from the given unit") 175 | c.fs.BoolVar(&c.follow, "follow", false, "Follow logs") 176 | c.fs.BoolVar(&c.follow, "f", false, "Follow logs") 177 | c.fs.BoolVar(&c.noDate, "no-date", false, "No date information") 178 | c.fs.BoolVar(&c.noSource, "no-source", false, "No source information") 179 | } 180 | return c.fs 181 | } 182 | -------------------------------------------------------------------------------- /tsuru/client/pool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "sort" 12 | "strings" 13 | 14 | "github.com/mitchellh/go-wordwrap" 15 | "github.com/tsuru/gnuflag" 16 | "github.com/tsuru/go-tsuruclient/pkg/config" 17 | "github.com/tsuru/tablecli" 18 | "github.com/tsuru/tsuru-client/tsuru/formatter" 19 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 20 | "github.com/tsuru/tsuru/cmd" 21 | ) 22 | 23 | type poolFilter struct { 24 | name string 25 | team string 26 | } 27 | 28 | type PoolList struct { 29 | fs *gnuflag.FlagSet 30 | filter poolFilter 31 | simplified bool 32 | json bool 33 | } 34 | 35 | type Pool struct { 36 | Name string 37 | Public bool 38 | Default bool 39 | Provisioner string 40 | Allowed map[string][]string 41 | } 42 | 43 | func (p *Pool) Kind() string { 44 | if p.Public { 45 | return "public" 46 | } 47 | if p.Default { 48 | return "default" 49 | } 50 | return "" 51 | } 52 | 53 | func (p *Pool) GetProvisioner() string { 54 | if p.Provisioner == "" { 55 | return "default" 56 | } 57 | return p.Provisioner 58 | } 59 | 60 | type poolEntriesList []Pool 61 | 62 | func (l poolEntriesList) Len() int { return len(l) } 63 | func (l poolEntriesList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 64 | func (l poolEntriesList) Less(i, j int) bool { 65 | cmp := strings.Compare(l[i].Kind(), l[j].Kind()) 66 | if cmp == 0 { 67 | return l[i].Name < l[j].Name 68 | } 69 | return cmp < 0 70 | } 71 | 72 | func (c *PoolList) Flags() *gnuflag.FlagSet { 73 | if c.fs == nil { 74 | c.fs = gnuflag.NewFlagSet("volume-list", gnuflag.ExitOnError) 75 | c.fs.StringVar(&c.filter.name, "name", "", "Filter pools by name") 76 | c.fs.StringVar(&c.filter.name, "n", "", "Filter pools by name") 77 | c.fs.StringVar(&c.filter.team, "team", "", "Filter pools by team ") 78 | c.fs.StringVar(&c.filter.team, "t", "", "Filter pools by team") 79 | c.fs.BoolVar(&c.simplified, "q", false, "Display only pools name") 80 | c.fs.BoolVar(&c.json, "json", false, "Display in JSON format") 81 | 82 | } 83 | return c.fs 84 | } 85 | 86 | func (pl *PoolList) Run(context *cmd.Context) error { 87 | url, err := config.GetURL("/pools") 88 | if err != nil { 89 | return err 90 | } 91 | request, err := http.NewRequest("GET", url, nil) 92 | if err != nil { 93 | return err 94 | } 95 | resp, err := tsuruHTTP.AuthenticatedClient.Do(request) 96 | if err != nil { 97 | return err 98 | } 99 | t := tablecli.Table{Headers: tablecli.Row([]string{"Pool", "Kind", "Provisioner", "Teams", "Routers"}), LineSeparator: true} 100 | if resp.StatusCode == http.StatusNoContent { 101 | context.Stdout.Write(t.Bytes()) 102 | return nil 103 | } 104 | defer resp.Body.Close() 105 | var pools []Pool 106 | err = json.NewDecoder(resp.Body).Decode(&pools) 107 | if err != nil { 108 | return err 109 | } 110 | sort.Sort(poolEntriesList(pools)) 111 | 112 | pools = pl.clientSideFilter(pools) 113 | 114 | if pl.simplified { 115 | for _, v := range pools { 116 | fmt.Fprintln(context.Stdout, v.Name) 117 | } 118 | return nil 119 | } 120 | 121 | if pl.json { 122 | return formatter.JSON(context.Stdout, pools) 123 | } 124 | 125 | for _, pool := range pools { 126 | teams := "" 127 | if !pool.Public && !pool.Default { 128 | teams = strings.Join(pool.Allowed["team"], ", ") 129 | } 130 | routers := strings.Join(pool.Allowed["router"], ", ") 131 | t.AddRow(tablecli.Row([]string{ 132 | pool.Name, 133 | pool.Kind(), 134 | pool.GetProvisioner(), 135 | wordwrap.WrapString(teams, 30), 136 | wordwrap.WrapString(routers, 30), 137 | })) 138 | } 139 | context.Stdout.Write(t.Bytes()) 140 | return nil 141 | } 142 | 143 | func (c *PoolList) clientSideFilter(pools []Pool) []Pool { 144 | result := make([]Pool, 0, len(pools)) 145 | 146 | for _, pool := range pools { 147 | insert := true 148 | if c.filter.name != "" && !strings.Contains(pool.Name, c.filter.name) { 149 | insert = false 150 | } 151 | 152 | if c.filter.team != "" && !sliceContains(pool.Allowed["team"], c.filter.team) { 153 | insert = false 154 | } 155 | 156 | if insert { 157 | result = append(result, pool) 158 | } 159 | } 160 | 161 | return result 162 | } 163 | 164 | func sliceContains(s []string, d string) bool { 165 | for _, i := range s { 166 | if i == d { 167 | return true 168 | } 169 | } 170 | 171 | return false 172 | } 173 | 174 | func (PoolList) Info() *cmd.Info { 175 | return &cmd.Info{ 176 | Name: "pool-list", 177 | Usage: "pool-list", 178 | Desc: "List all pools available for deploy.", 179 | MinArgs: 0, 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /tsuru/client/pool_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | 11 | "github.com/tsuru/tsuru/cmd" 12 | "github.com/tsuru/tsuru/cmd/cmdtest" 13 | "gopkg.in/check.v1" 14 | ) 15 | 16 | func (s *S) TestPoolListInfo(c *check.C) { 17 | c.Assert((&PoolList{}).Info(), check.NotNil) 18 | } 19 | 20 | func (s *S) TestPoolListRun(c *check.C) { 21 | var stdout, stderr bytes.Buffer 22 | result := `[{"Name":"theonepool","Public":true,"Default":true,"Allowed":{"router":["hipache"]}},{"Name":"pool1","Public":false,"Default":true},{"Name":"pool2","Public":false,"Default":false,"Allowed":{"team":["admin"]}},{"Name":"pool0","Public":false,"Default":false,"Allowed":{"team":["admin"]}},{"Name":"pool3","Public":false,"Default":false,"Provisioner":"swarm","Allowed":{"router":["hipache","planb"],"team":["admin","team1","team2","team3","team4","team5"]}}]` 23 | context := cmd.Context{ 24 | Args: []string{}, 25 | Stdout: &stdout, 26 | Stderr: &stderr, 27 | } 28 | expected := `+------------+---------+-------------+-----------------------------+----------------+ 29 | | Pool | Kind | Provisioner | Teams | Routers | 30 | +------------+---------+-------------+-----------------------------+----------------+ 31 | | pool0 | | default | admin | | 32 | +------------+---------+-------------+-----------------------------+----------------+ 33 | | pool2 | | default | admin | | 34 | +------------+---------+-------------+-----------------------------+----------------+ 35 | | pool3 | | swarm | admin, team1, team2, team3, | hipache, planb | 36 | | | | | team4, team5 | | 37 | +------------+---------+-------------+-----------------------------+----------------+ 38 | | pool1 | default | default | | | 39 | +------------+---------+-------------+-----------------------------+----------------+ 40 | | theonepool | public | default | | hipache | 41 | +------------+---------+-------------+-----------------------------+----------------+ 42 | ` 43 | s.setupFakeTransport(&cmdtest.Transport{Message: result, Status: http.StatusOK}) 44 | command := PoolList{} 45 | err := command.Run(&context) 46 | c.Assert(err, check.IsNil) 47 | c.Assert(stdout.String(), check.Equals, expected) 48 | } 49 | 50 | func (s *S) TestPoolListRunNoContent(c *check.C) { 51 | var stdout bytes.Buffer 52 | context := cmd.Context{Args: []string{}, Stdout: &stdout} 53 | s.setupFakeTransport(&cmdtest.Transport{Status: http.StatusNoContent}) 54 | command := PoolList{} 55 | err := command.Run(&context) 56 | expected := `+------+------+-------------+-------+---------+ 57 | | Pool | Kind | Provisioner | Teams | Routers | 58 | +------+------+-------------+-------+---------+ 59 | ` 60 | c.Assert(err, check.IsNil) 61 | c.Assert(stdout.String(), check.Equals, expected) 62 | } 63 | -------------------------------------------------------------------------------- /tsuru/client/run.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/tsuru/gnuflag" 16 | "github.com/tsuru/go-tsuruclient/pkg/config" 17 | tsuruClientApp "github.com/tsuru/tsuru-client/tsuru/app" 18 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 19 | "github.com/tsuru/tsuru/cmd" 20 | tsuruIo "github.com/tsuru/tsuru/io" 21 | ) 22 | 23 | type AppRun struct { 24 | tsuruClientApp.AppNameMixIn 25 | fs *gnuflag.FlagSet 26 | once bool 27 | isolated bool 28 | } 29 | 30 | func (c *AppRun) Info() *cmd.Info { 31 | desc := `Runs an arbitrary command in application's containers. The base directory for 32 | all commands is the root of the application. 33 | 34 | If you use the [[--once]] flag tsuru will run the command only in one unit. 35 | Otherwise, it will run the command in all units.` 36 | return &cmd.Info{ 37 | Name: "app-run", 38 | Usage: "app run [commandarg1] [commandarg2] ... [commandargn] [-a/--app appname] [-o/--once] [-i/--isolated]", 39 | Desc: desc, 40 | MinArgs: 1, 41 | } 42 | } 43 | 44 | func (c *AppRun) Run(context *cmd.Context) error { 45 | context.RawOutput() 46 | appName, err := c.AppNameByFlag() 47 | if err != nil { 48 | return err 49 | } 50 | u, err := config.GetURL(fmt.Sprintf("/apps/%s/run", appName)) 51 | if err != nil { 52 | return err 53 | } 54 | v := url.Values{} 55 | v.Set("command", strings.Join(context.Args, " ")) 56 | v.Set("once", strconv.FormatBool(c.once)) 57 | v.Set("isolated", strconv.FormatBool(c.isolated)) 58 | b := strings.NewReader(v.Encode()) 59 | request, err := http.NewRequest("POST", u, b) 60 | if err != nil { 61 | return err 62 | } 63 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded") 64 | r, err := tsuruHTTP.AuthenticatedClient.Do(request) 65 | if err != nil { 66 | return err 67 | } 68 | defer r.Body.Close() 69 | w := tsuruIo.NewStreamWriter(context.Stdout, &tsuruIo.SimpleJsonMessageFormatter{NoTimestamp: true}) 70 | for n := int64(1); n > 0 && err == nil; n, err = io.Copy(w, r.Body) { 71 | } 72 | if err != nil { 73 | return err 74 | } 75 | unparsed := w.Remaining() 76 | if len(unparsed) > 0 { 77 | return fmt.Errorf("unparsed message error: %s", string(unparsed)) 78 | } 79 | return nil 80 | } 81 | 82 | func (c *AppRun) Flags() *gnuflag.FlagSet { 83 | if c.fs == nil { 84 | c.fs = c.AppNameMixIn.Flags() 85 | c.fs.BoolVar(&c.once, "once", false, "Running only one unit") 86 | c.fs.BoolVar(&c.once, "o", false, "Running only one unit") 87 | c.fs.BoolVar(&c.isolated, "isolated", false, "Running in ephemeral container") 88 | c.fs.BoolVar(&c.isolated, "i", false, "Running in ephemeral container") 89 | } 90 | return c.fs 91 | } 92 | -------------------------------------------------------------------------------- /tsuru/client/run_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/tsuru/tsuru/cmd" 14 | "github.com/tsuru/tsuru/cmd/cmdtest" 15 | "github.com/tsuru/tsuru/io" 16 | "gopkg.in/check.v1" 17 | ) 18 | 19 | func (s *S) TestAppRun(c *check.C) { 20 | var stdout, stderr bytes.Buffer 21 | expected := "http.go http_test.go" 22 | context := cmd.Context{ 23 | Stdout: &stdout, 24 | Stderr: &stderr, 25 | } 26 | msg := io.SimpleJsonMessage{Message: expected} 27 | result, err := json.Marshal(msg) 28 | c.Assert(err, check.IsNil) 29 | trans := &cmdtest.ConditionalTransport{ 30 | Transport: cmdtest.Transport{ 31 | Message: string(result), 32 | Status: http.StatusOK, 33 | }, 34 | CondFunc: func(req *http.Request) bool { 35 | contentType := req.Header.Get("Content-Type") == "application/x-www-form-urlencoded" 36 | cmd := req.FormValue("command") == "ls" 37 | path := strings.HasSuffix(req.URL.Path, "/apps/ble/run") 38 | return path && cmd && contentType 39 | }, 40 | } 41 | s.setupFakeTransport(trans) 42 | command := AppRun{} 43 | err = command.Flags().Parse(true, []string{"--app", "ble", "ls"}) 44 | c.Assert(err, check.IsNil) 45 | 46 | context.Args = command.Flags().Args() 47 | err = command.Run(&context) 48 | c.Assert(err, check.IsNil) 49 | c.Assert(stdout.String(), check.Equals, expected) 50 | } 51 | 52 | func (s *S) TestAppRunFlagIsolated(c *check.C) { 53 | var stdout, stderr bytes.Buffer 54 | expected := "http.go http_test.go" 55 | context := cmd.Context{ 56 | Stdout: &stdout, 57 | Stderr: &stderr, 58 | } 59 | msg := io.SimpleJsonMessage{Message: expected} 60 | result, err := json.Marshal(msg) 61 | c.Assert(err, check.IsNil) 62 | trans := &cmdtest.ConditionalTransport{ 63 | Transport: cmdtest.Transport{ 64 | Message: string(result), 65 | Status: http.StatusOK, 66 | }, 67 | CondFunc: func(req *http.Request) bool { 68 | contentType := req.Header.Get("Content-Type") == "application/x-www-form-urlencoded" 69 | cmd := req.FormValue("isolated") == "true" 70 | path := strings.HasSuffix(req.URL.Path, "/apps/ble/run") 71 | return path && cmd && contentType 72 | }, 73 | } 74 | s.setupFakeTransport(trans) 75 | command := AppRun{} 76 | err = command.Flags().Parse(true, []string{"--app", "ble", "--isolated", "ls"}) 77 | c.Assert(err, check.IsNil) 78 | 79 | context.Args = command.Flags().Args() 80 | err = command.Run(&context) 81 | c.Assert(err, check.IsNil) 82 | c.Assert(stdout.String(), check.Equals, expected) 83 | } 84 | 85 | func (s *S) TestAppRunShouldUseAllSubsequentArgumentsAsArgumentsToTheGivenCommand(c *check.C) { 86 | var stdout, stderr bytes.Buffer 87 | expected := "-rw-r--r-- 1 f staff 119 Apr 26 18:23 http.go\n" 88 | context := cmd.Context{ 89 | Stdout: &stdout, 90 | Stderr: &stderr, 91 | } 92 | msg := io.SimpleJsonMessage{Message: expected} 93 | result, err := json.Marshal(msg) 94 | c.Assert(err, check.IsNil) 95 | trans := &cmdtest.ConditionalTransport{ 96 | Transport: cmdtest.Transport{ 97 | Message: string(result) + "\n" + string(result), 98 | Status: http.StatusOK, 99 | }, 100 | CondFunc: func(req *http.Request) bool { 101 | cmd := req.FormValue("command") == "ls -l" 102 | path := strings.HasSuffix(req.URL.Path, "/apps/ble/run") 103 | contentType := req.Header.Get("Content-Type") == "application/x-www-form-urlencoded" 104 | return cmd && path && contentType 105 | }, 106 | } 107 | s.setupFakeTransport(trans) 108 | command := AppRun{} 109 | err = command.Flags().Parse(true, []string{"--app", "ble", "ls -l"}) 110 | 111 | c.Assert(err, check.IsNil) 112 | 113 | context.Args = command.Flags().Args() 114 | 115 | err = command.Run(&context) 116 | c.Assert(err, check.IsNil) 117 | c.Assert(stdout.String(), check.Equals, expected+expected) 118 | } 119 | 120 | func (s *S) TestAppRunWithoutTheFlag(c *check.C) { 121 | var stdout, stderr bytes.Buffer 122 | expected := "-rw-r--r-- 1 f staff 119 Apr 26 18:23 http.go" 123 | context := cmd.Context{ 124 | Stdout: &stdout, 125 | Stderr: &stderr, 126 | } 127 | msg := io.SimpleJsonMessage{Message: expected} 128 | result, err := json.Marshal(msg) 129 | c.Assert(err, check.IsNil) 130 | trans := &cmdtest.ConditionalTransport{ 131 | Transport: cmdtest.Transport{ 132 | Message: string(result), 133 | Status: http.StatusOK, 134 | }, 135 | CondFunc: func(req *http.Request) bool { 136 | path := strings.HasSuffix(req.URL.Path, "/apps/bla/run") 137 | cmd := req.FormValue("command") == "ls -lh" 138 | contentType := req.Header.Get("Content-Type") == "application/x-www-form-urlencoded" 139 | return path && cmd && contentType 140 | }, 141 | } 142 | s.setupFakeTransport(trans) 143 | command := AppRun{} 144 | err = command.Flags().Parse(true, []string{"-a", "bla", "ls -lh"}) 145 | c.Assert(err, check.IsNil) 146 | 147 | context.Args = command.Flags().Args() 148 | err = command.Run(&context) 149 | c.Assert(err, check.IsNil) 150 | c.Assert(stdout.String(), check.Equals, expected) 151 | } 152 | 153 | func (s *S) TestAppRunShouldReturnErrorWhenCommandGoWrong(c *check.C) { 154 | var stdout, stderr bytes.Buffer 155 | context := cmd.Context{ 156 | Stdout: &stdout, 157 | Stderr: &stderr, 158 | } 159 | msg := io.SimpleJsonMessage{Error: "command doesn't exist."} 160 | result, err := json.Marshal(msg) 161 | c.Assert(err, check.IsNil) 162 | trans := &cmdtest.ConditionalTransport{ 163 | Transport: cmdtest.Transport{ 164 | Message: string(result), 165 | Status: http.StatusOK, 166 | }, 167 | CondFunc: func(req *http.Request) bool { 168 | return strings.HasSuffix(req.URL.Path, "/apps/bla/run") 169 | }, 170 | } 171 | s.setupFakeTransport(trans) 172 | command := AppRun{} 173 | err = command.Flags().Parse(true, []string{"-a", "bla", "cmd_error"}) 174 | c.Assert(err, check.IsNil) 175 | 176 | context.Args = command.Flags().Args() 177 | 178 | err = command.Run(&context) 179 | c.Assert(err, check.ErrorMatches, "command doesn't exist.") 180 | } 181 | 182 | func (s *S) TestAppRunInfo(c *check.C) { 183 | command := AppRun{} 184 | c.Assert(command.Info(), check.NotNil) 185 | } 186 | -------------------------------------------------------------------------------- /tsuru/client/shell.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "os/signal" 14 | "regexp" 15 | "strconv" 16 | "syscall" 17 | 18 | "github.com/tsuru/gnuflag" 19 | "github.com/tsuru/go-tsuruclient/pkg/config" 20 | tsuruClientApp "github.com/tsuru/tsuru-client/tsuru/app" 21 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 22 | "github.com/tsuru/tsuru/cmd" 23 | "golang.org/x/net/websocket" 24 | terminal "golang.org/x/term" 25 | ) 26 | 27 | var httpRegexp = regexp.MustCompile(`^http`) 28 | 29 | type ShellToContainerCmd struct { 30 | tsuruClientApp.AppNameMixIn 31 | isolated bool 32 | debug bool 33 | fs *gnuflag.FlagSet 34 | } 35 | 36 | func (c *ShellToContainerCmd) Info() *cmd.Info { 37 | return &cmd.Info{ 38 | Name: "app-shell", 39 | Usage: "app-shell [unit-id] -a/--app [-i/--isolated]", 40 | Desc: `Opens a remote shell inside unit, using the API server as a proxy. You 41 | can access an app unit just giving app name, or specifying the id of the unit. 42 | You can get the ID of the unit using the app-info command.`, 43 | MinArgs: 0, 44 | } 45 | } 46 | 47 | func (c *ShellToContainerCmd) Flags() *gnuflag.FlagSet { 48 | if c.fs == nil { 49 | c.fs = c.AppNameMixIn.Flags() 50 | help := "Run shell in a new unit" 51 | c.fs.BoolVar(&c.isolated, "isolated", false, help) 52 | c.fs.BoolVar(&c.isolated, "i", false, help) 53 | c.fs.BoolVar(&c.debug, "debug", false, "Enable debug mode") 54 | c.fs.BoolVar(&c.debug, "d", false, "Enable debug mode") 55 | } 56 | return c.fs 57 | } 58 | 59 | type descriptable interface { 60 | Fd() uintptr 61 | } 62 | 63 | func (c *ShellToContainerCmd) Run(context *cmd.Context) error { 64 | appName, err := c.AppNameByFlag() 65 | if err != nil { 66 | return err 67 | } 68 | appInfoURL, err := config.GetURL(fmt.Sprintf("/apps/%s", appName)) 69 | if err != nil { 70 | return err 71 | } 72 | request, err := http.NewRequest("GET", appInfoURL, nil) 73 | if err != nil { 74 | return err 75 | } 76 | _, err = tsuruHTTP.AuthenticatedClient.Do(request) 77 | if err != nil { 78 | return err 79 | } 80 | context.RawOutput() 81 | var width, height int 82 | if desc, ok := context.Stdin.(descriptable); ok { 83 | fd := int(desc.Fd()) 84 | if terminal.IsTerminal(fd) { 85 | width, height, _ = terminal.GetSize(fd) 86 | oldState, terminalErr := terminal.MakeRaw(fd) 87 | if terminalErr != nil { 88 | return err 89 | } 90 | defer terminal.Restore(fd, oldState) 91 | sigChan := make(chan os.Signal, 2) 92 | go func(c <-chan os.Signal) { 93 | if _, ok := <-c; ok { 94 | terminal.Restore(fd, oldState) 95 | os.Exit(1) 96 | } 97 | }(sigChan) 98 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 99 | } 100 | } 101 | queryString := make(url.Values) 102 | queryString.Set("isolated", strconv.FormatBool(c.isolated)) 103 | queryString.Set("debug", strconv.FormatBool(c.debug)) 104 | queryString.Set("width", strconv.Itoa(width)) 105 | queryString.Set("height", strconv.Itoa(height)) 106 | if len(context.Args) > 0 { 107 | queryString.Set("unit", context.Args[0]) 108 | queryString.Set("container_id", context.Args[0]) 109 | } 110 | if term := os.Getenv("TERM"); term != "" { 111 | queryString.Set("term", term) 112 | } 113 | serverURL, err := config.GetURL(fmt.Sprintf("/apps/%s/shell?%s", appName, queryString.Encode())) 114 | if err != nil { 115 | return err 116 | } 117 | serverURL = httpRegexp.ReplaceAllString(serverURL, "ws") 118 | wsConfig, err := websocket.NewConfig(serverURL, "ws://localhost") 119 | if err != nil { 120 | return err 121 | } 122 | var token string 123 | if token, err = config.DefaultTokenProvider.Token(); err == nil { 124 | wsConfig.Header.Set("Authorization", "bearer "+token) 125 | } 126 | conn, err := websocket.DialConfig(wsConfig) 127 | if err != nil { 128 | return err 129 | } 130 | defer conn.Close() 131 | errs := make(chan error, 2) 132 | quit := make(chan bool) 133 | go io.Copy(conn, context.Stdin) 134 | go func() { 135 | defer close(quit) 136 | _, err := io.Copy(context.Stdout, conn) 137 | if err != nil && err != io.EOF { 138 | errs <- err 139 | } 140 | }() 141 | <-quit 142 | close(errs) 143 | return <-errs 144 | } 145 | -------------------------------------------------------------------------------- /tsuru/client/shell_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | "net/http/httptest" 11 | "os" 12 | 13 | "github.com/tsuru/tsuru/cmd" 14 | "github.com/tsuru/tsuru/cmd/cmdtest" 15 | "golang.org/x/net/websocket" 16 | check "gopkg.in/check.v1" 17 | ) 18 | 19 | func buildHandler(content []byte) websocket.Handler { 20 | return websocket.Handler(func(conn *websocket.Conn) { 21 | conn.Write(content) 22 | conn.Close() 23 | }) 24 | } 25 | 26 | func (s *S) TestShellToContainerCmdInfo(c *check.C) { 27 | var command ShellToContainerCmd 28 | info := command.Info() 29 | c.Assert(info, check.NotNil) 30 | } 31 | 32 | func (s *S) TestShellToContainerCmdRunWithApp(c *check.C) { 33 | transport := cmdtest.ConditionalTransport{ 34 | Transport: cmdtest.Transport{ 35 | Message: "", 36 | Status: http.StatusOK, 37 | }, 38 | CondFunc: func(req *http.Request) bool { 39 | return req.Method == "GET" && req.URL.Path == "/1.0/apps/myapp" 40 | }, 41 | } 42 | server := httptest.NewServer(buildHandler([]byte("hello my friend\nglad to see you here\n"))) 43 | defer server.Close() 44 | target := "http://" + server.Listener.Addr().String() 45 | os.Setenv("TSURU_TARGET", target) 46 | defer os.Unsetenv("TSURU_TARGET") 47 | os.Setenv("TSURU_TOKEN", "abc123") 48 | defer os.Unsetenv("TSURU_TOKEN") 49 | var stdout, stderr, stdin bytes.Buffer 50 | context := cmd.Context{ 51 | Stdout: &stdout, 52 | Stderr: &stderr, 53 | Stdin: &stdin, 54 | } 55 | var command ShellToContainerCmd 56 | err := command.Flags().Parse(true, []string{"-a", "myapp"}) 57 | c.Assert(err, check.IsNil) 58 | s.setupFakeTransport(&transport) 59 | err = command.Run(&context) 60 | c.Assert(err, check.IsNil) 61 | c.Assert(stdout.String(), check.Equals, "hello my friend\nglad to see you here\n") 62 | } 63 | 64 | func (s *S) TestShellToContainerWithUnit(c *check.C) { 65 | transport := cmdtest.ConditionalTransport{ 66 | Transport: cmdtest.Transport{ 67 | Message: "", 68 | Status: http.StatusOK, 69 | }, 70 | CondFunc: func(req *http.Request) bool { 71 | return req.Method == "GET" && req.URL.Path == "/1.0/apps/myapp" 72 | }, 73 | } 74 | server := httptest.NewServer(buildHandler([]byte("hello my friend\nglad to see you here\n"))) 75 | defer server.Close() 76 | target := "http://" + server.Listener.Addr().String() 77 | os.Setenv("TSURU_TARGET", target) 78 | defer os.Unsetenv("TSURU_TARGET") 79 | os.Setenv("TSURU_TOKEN", "abc123") 80 | defer os.Unsetenv("TSURU_TOKEN") 81 | var stdout, stderr, stdin bytes.Buffer 82 | context := cmd.Context{ 83 | Args: []string{"containerid"}, 84 | Stdout: &stdout, 85 | Stderr: &stderr, 86 | Stdin: &stdin, 87 | } 88 | var command ShellToContainerCmd 89 | err := command.Flags().Parse(true, []string{"-a", "myapp"}) 90 | c.Assert(err, check.IsNil) 91 | 92 | s.setupFakeTransport(&transport) 93 | err = command.Run(&context) 94 | c.Assert(err, check.IsNil) 95 | c.Assert(stdout.String(), check.Equals, "hello my friend\nglad to see you here\n") 96 | } 97 | 98 | func (s *S) TestShellToContainerCmdConnectionRefused(c *check.C) { 99 | var buf bytes.Buffer 100 | transport := cmdtest.ConditionalTransport{ 101 | Transport: cmdtest.Transport{ 102 | Message: "", 103 | Status: http.StatusOK, 104 | }, 105 | CondFunc: func(req *http.Request) bool { 106 | return req.Method == "GET" && req.URL.Path == "/apps/cmd" 107 | }, 108 | } 109 | server := httptest.NewServer(nil) 110 | addr := server.Listener.Addr().String() 111 | server.Close() 112 | os.Setenv("TSURU_TARGET", "http://"+addr) 113 | defer os.Unsetenv("TSURU_TARGET") 114 | os.Setenv("TSURU_TOKEN", "abc123") 115 | defer os.Unsetenv("TSURU_TOKEN") 116 | context := cmd.Context{ 117 | Args: []string{"af3332d"}, 118 | Stdout: &buf, 119 | Stderr: &buf, 120 | Stdin: &buf, 121 | } 122 | 123 | s.setupFakeTransport(&transport) 124 | var command ShellToContainerCmd 125 | err := command.Run(&context) 126 | c.Assert(err, check.NotNil) 127 | } 128 | 129 | func (s *S) TestShellToContainerSessionExpired(c *check.C) { 130 | var stdout, stderr, stdin bytes.Buffer 131 | context := cmd.Context{ 132 | Args: []string{"containerid"}, 133 | Stdout: &stdout, 134 | Stderr: &stderr, 135 | Stdin: &stdin, 136 | } 137 | transport := cmdtest.ConditionalTransport{ 138 | Transport: cmdtest.Transport{ 139 | Message: "", 140 | Status: http.StatusUnauthorized, 141 | }, 142 | CondFunc: func(req *http.Request) bool { 143 | return req.Method == "GET" && req.URL.Path == "/1.0/apps/myapp" 144 | }, 145 | } 146 | var command ShellToContainerCmd 147 | err := command.Flags().Parse(true, []string{"-a", "myapp"}) 148 | c.Assert(err, check.IsNil) 149 | 150 | s.setupFakeTransport(&transport) 151 | err = command.Run(&context) 152 | c.Assert(err, check.NotNil) 153 | c.Assert(err, check.ErrorMatches, ".*Unauthorized") 154 | } 155 | -------------------------------------------------------------------------------- /tsuru/client/suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "net/http" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/cezarsa/form" 14 | "github.com/tsuru/go-tsuruclient/pkg/config" 15 | "github.com/tsuru/tsuru-client/tsuru/formatter" 16 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 17 | "gopkg.in/check.v1" 18 | ) 19 | 20 | type S struct { 21 | defaultLocation time.Location 22 | t *testing.T 23 | } 24 | 25 | func (s *S) SetUpSuite(c *check.C) { 26 | form.DefaultEncoder = form.DefaultEncoder.UseJSONTags(false) 27 | form.DefaultDecoder = form.DefaultDecoder.UseJSONTags(false) 28 | } 29 | 30 | func (s *S) SetUpTest(c *check.C) { 31 | os.Setenv("TSURU_TARGET", "http://localhost:8080") 32 | os.Setenv("TSURU_TOKEN", "sometoken") 33 | s.defaultLocation = *formatter.LocalTZ 34 | location, err := time.LoadLocation("US/Central") 35 | if err == nil { 36 | formatter.LocalTZ = location 37 | } 38 | config.ResetFileSystem() 39 | } 40 | 41 | func (s *S) TearDownTest(c *check.C) { 42 | os.Unsetenv("TSURU_TARGET") 43 | os.Unsetenv("TSURU_TOKEN") 44 | formatter.LocalTZ = &s.defaultLocation 45 | } 46 | 47 | var suite = &S{} 48 | var _ = check.Suite(suite) 49 | 50 | func Test(t *testing.T) { 51 | suite.t = t 52 | check.TestingT(t) 53 | } 54 | 55 | func (s *S) setupFakeTransport(rt http.RoundTripper) { 56 | tsuruHTTP.AuthenticatedClient = tsuruHTTP.NewTerminalClient(tsuruHTTP.TerminalClientOptions{ 57 | RoundTripper: rt, 58 | ClientName: "test", 59 | ClientVersion: "0.1.0", 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /tsuru/client/tag.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "sort" 13 | "strings" 14 | 15 | "github.com/tsuru/go-tsuruclient/pkg/config" 16 | "github.com/tsuru/tablecli" 17 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 18 | "github.com/tsuru/tsuru/cmd" 19 | "github.com/tsuru/tsuru/service" 20 | ) 21 | 22 | type TagList struct{} 23 | 24 | type tag struct { 25 | Name string 26 | Apps []string 27 | ServiceInstances map[string][]string 28 | } 29 | 30 | func (t *TagList) Info() *cmd.Info { 31 | return &cmd.Info{ 32 | Name: "tag-list", 33 | Usage: "tag-list", 34 | Desc: `Retrieves and shows a list of tags with the respective apps and service instances.`, 35 | } 36 | } 37 | 38 | func (t *TagList) Run(context *cmd.Context) error { 39 | apps, err := loadApps() 40 | if err != nil { 41 | return err 42 | } 43 | services, err := loadServices() 44 | if err != nil { 45 | return err 46 | } 47 | return t.Show(apps, services, context) 48 | } 49 | 50 | func (t *TagList) Show(apps []app, services []service.ServiceModel, context *cmd.Context) error { 51 | tagList := processTags(apps, services) 52 | if len(tagList) == 0 { 53 | return nil 54 | } 55 | table := tablecli.NewTable() 56 | table.Headers = tablecli.Row([]string{"Tag", "Apps", "Service Instances"}) 57 | for _, tagName := range sortedTags(tagList) { 58 | t := tagList[tagName] 59 | var instanceNames []string 60 | for _, serviceName := range sortedServices(t.ServiceInstances) { 61 | instances := t.ServiceInstances[serviceName] 62 | for _, instanceName := range instances { 63 | instanceNames = append(instanceNames, fmt.Sprintf("%s: %s", serviceName, instanceName)) 64 | } 65 | } 66 | table.AddRow(tablecli.Row([]string{t.Name, strings.Join(t.Apps, "\n"), strings.Join(instanceNames, "\n")})) 67 | } 68 | table.LineSeparator = true 69 | table.Sort() 70 | context.Stdout.Write(table.Bytes()) 71 | return nil 72 | } 73 | 74 | func loadApps() ([]app, error) { 75 | result, err := getFromURL("/apps") 76 | if err != nil { 77 | return nil, err 78 | } 79 | var apps []app 80 | err = json.Unmarshal(result, &apps) 81 | return apps, err 82 | } 83 | 84 | func loadServices() ([]service.ServiceModel, error) { 85 | result, err := getFromURL("/services") 86 | if err != nil { 87 | return nil, err 88 | } 89 | var services []service.ServiceModel 90 | err = json.Unmarshal(result, &services) 91 | return services, err 92 | } 93 | 94 | func getFromURL(path string) ([]byte, error) { 95 | url, err := config.GetURL(path) 96 | if err != nil { 97 | return nil, err 98 | } 99 | request, err := http.NewRequest("GET", url, nil) 100 | if err != nil { 101 | return nil, err 102 | } 103 | response, err := tsuruHTTP.AuthenticatedClient.Do(request) 104 | if err != nil { 105 | return nil, err 106 | } 107 | defer response.Body.Close() 108 | return io.ReadAll(response.Body) 109 | } 110 | 111 | func processTags(apps []app, services []service.ServiceModel) map[string]*tag { 112 | tagList := make(map[string]*tag) 113 | for _, app := range apps { 114 | for _, t := range app.Tags { 115 | if _, ok := tagList[t]; !ok { 116 | tagList[t] = &tag{Name: t, Apps: []string{app.Name}} 117 | } else { 118 | tagList[t].Apps = append(tagList[t].Apps, app.Name) 119 | } 120 | } 121 | } 122 | for _, s := range services { 123 | for _, instance := range s.ServiceInstances { 124 | for _, t := range instance.Tags { 125 | if _, ok := tagList[t]; !ok { 126 | tagList[t] = &tag{Name: t, ServiceInstances: make(map[string][]string)} 127 | } 128 | si := &tagList[t].ServiceInstances 129 | if *si == nil { 130 | *si = make(map[string][]string) 131 | } 132 | (*si)[s.Service] = append((*si)[s.Service], instance.Name) 133 | } 134 | } 135 | } 136 | return tagList 137 | } 138 | 139 | func sortedTags(tagList map[string]*tag) []string { 140 | tagNames := make([]string, len(tagList)) 141 | i := 0 142 | for t := range tagList { 143 | tagNames[i] = t 144 | i++ 145 | } 146 | sort.Strings(tagNames) 147 | return tagNames 148 | } 149 | 150 | func sortedServices(services map[string][]string) []string { 151 | serviceNames := make([]string, len(services)) 152 | i := 0 153 | for s := range services { 154 | serviceNames[i] = s 155 | i++ 156 | } 157 | sort.Strings(serviceNames) 158 | return serviceNames 159 | } 160 | -------------------------------------------------------------------------------- /tsuru/client/tag_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | 11 | tsuruHTTP "github.com/tsuru/tsuru-client/tsuru/http" 12 | "github.com/tsuru/tsuru/cmd" 13 | "github.com/tsuru/tsuru/cmd/cmdtest" 14 | check "gopkg.in/check.v1" 15 | ) 16 | 17 | func (s *S) TestTagListInfo(c *check.C) { 18 | c.Assert((&TagList{}).Info(), check.NotNil) 19 | } 20 | 21 | func (s *S) TestTagListWithApps(c *check.C) { 22 | var stdout, stderr bytes.Buffer 23 | appList := `[{"name":"app1","tags":["tag1"]},{"name":"app2","tags":["tag2","tag3"]},{"name":"app3","tags":[]},{"name":"app4","tags":["tag1","tag3"]}]` 24 | serviceList := "[]" 25 | expected := `+------+------+-------------------+ 26 | | Tag | Apps | Service Instances | 27 | +------+------+-------------------+ 28 | | tag1 | app1 | | 29 | | | app4 | | 30 | +------+------+-------------------+ 31 | | tag2 | app2 | | 32 | +------+------+-------------------+ 33 | | tag3 | app2 | | 34 | | | app4 | | 35 | +------+------+-------------------+ 36 | ` 37 | context := cmd.Context{ 38 | Args: []string{}, 39 | Stdout: &stdout, 40 | Stderr: &stderr, 41 | } 42 | command := TagList{} 43 | s.setupFakeTransport(makeTransport([]string{appList, serviceList})) 44 | err := command.Run(&context) 45 | c.Assert(err, check.IsNil) 46 | c.Assert(stdout.String(), check.Equals, expected) 47 | } 48 | 49 | func (s *S) TestTagListWithServiceInstances(c *check.C) { 50 | var stdout, stderr bytes.Buffer 51 | appList := "[]" 52 | serviceList := `[{"service":"service1","service_instances":[{"name":"instance1","tags":["tag1"]},{"name":"instance2","tags":[]},{"name":"instance3","tags":["tag1","tag2"]}]},{"service":"service2","service_instances":[{"name":"instance4","tags":["tag1"]}]}]` 53 | expected := `+------+------+---------------------+ 54 | | Tag | Apps | Service Instances | 55 | +------+------+---------------------+ 56 | | tag1 | | service1: instance1 | 57 | | | | service1: instance3 | 58 | | | | service2: instance4 | 59 | +------+------+---------------------+ 60 | | tag2 | | service1: instance3 | 61 | +------+------+---------------------+ 62 | ` 63 | context := cmd.Context{ 64 | Args: []string{}, 65 | Stdout: &stdout, 66 | Stderr: &stderr, 67 | } 68 | command := TagList{} 69 | s.setupFakeTransport(makeTransport([]string{appList, serviceList})) 70 | err := command.Run(&context) 71 | c.Assert(err, check.IsNil) 72 | c.Assert(stdout.String(), check.Equals, expected) 73 | } 74 | 75 | func (s *S) TestTagListWithAppsAndServiceInstances(c *check.C) { 76 | var stdout, stderr bytes.Buffer 77 | appList := `[{"name":"app1","tags":["tag1"]},{"name":"app2","tags":["tag2","tag3"]},{"name":"app3","tags":[]},{"name":"app4","tags":["tag1","tag3"]}]` 78 | serviceList := `[{"service":"service1","service_instances":[{"name":"instance1","tags":["tag1"]},{"name":"instance2","tags":[]},{"name":"instance3","tags":["tag1","tag2"]}]}]` 79 | expected := `+------+------+---------------------+ 80 | | Tag | Apps | Service Instances | 81 | +------+------+---------------------+ 82 | | tag1 | app1 | service1: instance1 | 83 | | | app4 | service1: instance3 | 84 | +------+------+---------------------+ 85 | | tag2 | app2 | service1: instance3 | 86 | +------+------+---------------------+ 87 | | tag3 | app2 | | 88 | | | app4 | | 89 | +------+------+---------------------+ 90 | ` 91 | context := cmd.Context{ 92 | Args: []string{}, 93 | Stdout: &stdout, 94 | Stderr: &stderr, 95 | } 96 | command := TagList{} 97 | s.setupFakeTransport(makeTransport([]string{appList, serviceList})) 98 | err := command.Run(&context) 99 | c.Assert(err, check.IsNil) 100 | c.Assert(stdout.String(), check.Equals, expected) 101 | } 102 | 103 | func (s *S) TestTagListWithEmptyResponse(c *check.C) { 104 | var stdout, stderr bytes.Buffer 105 | appList := `[{"name":"app1","tags":[]}]` 106 | serviceList := `[{"service_instances":[{"name":"service1","tags":[]}]}]` 107 | expected := "" 108 | context := cmd.Context{ 109 | Args: []string{}, 110 | Stdout: &stdout, 111 | Stderr: &stderr, 112 | } 113 | command := TagList{} 114 | s.setupFakeTransport(makeTransport([]string{appList, serviceList})) 115 | err := command.Run(&context) 116 | c.Assert(err, check.IsNil) 117 | c.Assert(stdout.String(), check.Equals, expected) 118 | } 119 | 120 | func (s *S) TestTagListRequestError(c *check.C) { 121 | var stdout, stderr bytes.Buffer 122 | context := cmd.Context{ 123 | Args: []string{}, 124 | Stdout: &stdout, 125 | Stderr: &stderr, 126 | } 127 | command := TagList{} 128 | s.setupFakeTransport(&cmdtest.ConditionalTransport{ 129 | Transport: cmdtest.Transport{Status: http.StatusBadGateway}, 130 | CondFunc: func(*http.Request) bool { return true }, 131 | }) 132 | err := command.Run(&context) 133 | c.Assert(err, check.NotNil) 134 | c.Assert(tsuruHTTP.UnwrapErr(err).Error(), check.Equals, "502 Bad Gateway") 135 | c.Assert(stdout.String(), check.Equals, "") 136 | } 137 | 138 | func makeTransport(messages []string) http.RoundTripper { 139 | trueFunc := func(*http.Request) bool { return true } 140 | cts := make([]cmdtest.ConditionalTransport, len(messages)) 141 | for i, message := range messages { 142 | cts[i] = cmdtest.ConditionalTransport{ 143 | Transport: cmdtest.Transport{Message: message, Status: http.StatusOK}, 144 | CondFunc: trueFunc, 145 | } 146 | } 147 | 148 | return &cmdtest.MultiConditionalTransport{ConditionalTransports: cts} 149 | } 150 | -------------------------------------------------------------------------------- /tsuru/client/testdata-symlink/link: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /tsuru/client/testdata-symlink/test/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuru/tsuru-client/d2aaacd361307dcbbc0d4a838892714cfeaf4196/tsuru/client/testdata-symlink/test/index.html -------------------------------------------------------------------------------- /tsuru/client/testdata/.tsuru/plugins/myplugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuru/tsuru-client/d2aaacd361307dcbbc0d4a838892714cfeaf4196/tsuru/client/testdata/.tsuru/plugins/myplugin -------------------------------------------------------------------------------- /tsuru/client/testdata/.tsuru/plugins/otherplugin.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuru/tsuru-client/d2aaacd361307dcbbc0d4a838892714cfeaf4196/tsuru/client/testdata/.tsuru/plugins/otherplugin.exe -------------------------------------------------------------------------------- /tsuru/client/testdata/archivedplugins/myplugin.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuru/tsuru-client/d2aaacd361307dcbbc0d4a838892714cfeaf4196/tsuru/client/testdata/archivedplugins/myplugin.tar.gz -------------------------------------------------------------------------------- /tsuru/client/testdata/archivedplugins/myplugin.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuru/tsuru-client/d2aaacd361307dcbbc0d4a838892714cfeaf4196/tsuru/client/testdata/archivedplugins/myplugin.zip -------------------------------------------------------------------------------- /tsuru/client/testdata/cert/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDkzCCAnugAwIBAgIJAIN09j/dhfmsMA0GCSqGSIb3DQEBCwUAMGAxCzAJBgNV 3 | BAYTAkJSMRcwFQYDVQQIDA5SaW8gZGUgSmFuZWlybzEXMBUGA1UEBwwOUmlvIGRl 4 | IEphbmVpcm8xDjAMBgNVBAoMBVRzdXJ1MQ8wDQYDVQQDDAZhcHAuaW8wHhcNMTcw 5 | MTEyMjAzMzExWhcNMjcwMTEwMjAzMzExWjBgMQswCQYDVQQGEwJCUjEXMBUGA1UE 6 | CAwOUmlvIGRlIEphbmVpcm8xFzAVBgNVBAcMDlJpbyBkZSBKYW5laXJvMQ4wDAYD 7 | VQQKDAVUc3VydTEPMA0GA1UEAwwGYXBwLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOC 8 | AQ8AMIIBCgKCAQEAw3GRuXOyL0Ar5BYA8DAPkY7ZHtHpEFK5bOoZB3lLBMjIbUKk 9 | +riNTTgcY1eCsoAMZ0ZGmwmK/8mrJSBcsK/f1HVTcsSU0pA961ROPkAad/X/luSL 10 | nXxDnZ1c0cOeU3GC4limB4CSZ64SZEDJvkUWnhUjTO4jfOCu0brkEnF8x3fpxfAy 11 | OrAO50Uxij3VOQIAkP5B0T6x2Htr1ogm/vuubp5IG+KVuJHbozoaFFgRnDwrk+3W 12 | k3FFUvg4ywY2jgJMLFJb0U3IIQgSqwQwXftKdu1EaoxA5fQmu/3a4CvYKKkwLJJ+ 13 | 6L4O9Uf+QgaBZqTpDJ7XcIYbW+TPffzSwuI5PwIDAQABo1AwTjAdBgNVHQ4EFgQU 14 | 3XOK6bQW7hL47fMYH8JT/qCqIDgwHwYDVR0jBBgwFoAU3XOK6bQW7hL47fMYH8JT 15 | /qCqIDgwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAgP4K9Zd1xSOQ 16 | HAC6p2XjuveBI9Aswudaqg8ewYZtbtcbV70db+A69b8alSXfqNVqI4L2T97x/g6J 17 | 8ef8MG6TExhd1QktqtxtR+wsiijfUkityj8j5JT36TX3Kj0eIXrLJWxPEBhtGL17 18 | ZBGdNK2/tDsQl5Wb+qnz5Ge9obybRLHHL2L5mrSwb+nC+nrC2nlfjJgVse9HhU9j 19 | 6Euq5hstXAlQH7fUbC5zAMS5UFrbzR+hOvjrSwzkkJmKW8BKKCfSaevRhq4VXxpw 20 | Wx1oQV8UD5KLQQRy9Xew/KRHVzOpdkK66/i/hgV7GdREy4aKNAEBRpheOzjLDQyG 21 | YRLI1QVj1Q== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /tsuru/client/testdata/cert/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQDDI1f+n7uf+EdyWcm4gfe5Am5zSOU0XxP07D3iMJjAFMuFmknn 3 | SX2iZmtcCEQn4RTTab39YjLq5ibU0tk5KfpqjvPvGahp28paDe3g3p04fFkQv+ek 4 | z1ZVzgZM0utL/gaJ6Bfvpkkpn+rr2GgEXYk264wQb+Zi2h78Ypf6Itpp0QIDAQAB 5 | AoGAduwfQGoQallhEWtu6Ccs1W+J6HBQXy5idy1SOXrsXINP1UhGKdI74rEQVLIk 6 | 9zjQ/FbBFp618Tn6CFHHWHMgzKa6DeZaHBgXmSTahWj6R4fKv0njQXTMDFZ/jVTl 7 | 8UslNFHrErvMk2wghKtO32LUk1uKT2bVOumTK5z+JP7aOAECQQDxlXPCPYRlygPR 8 | 1GAP590u7s3Gyv/Ii6DZNc7/aTNbnV5dAGeIer1Ua2W+qEZYvhO/WvI5l88jI4Li 9 | dSuiqxYxAkEAzshdV4UiakLIqAOX6THnr3EjfCjNLQ92QQM7Kc/X9532jePxZSmf 10 | vy3iyONjWag0K5Db2jw1bJeAraIJndqFoQJAf8OuqPen4b1pL7vF4iOaEowxQAV0 11 | KTfPJZETnHiitL0RftYL614ea1sxQBf2vFAqWXVbzaG/5rGNMv8MyMb6wQJAZahV 12 | U0CNccYRVaAmn6s8JqEte82nSN7QGRgYju6yUvaijpEgTMaQ1XEei/pWDm7F7yER 13 | JJHzBcbZqQL/TU5v4QJBANKXzVtFg85IcFGQNTmWStiDGwUZJu7HmU+EC6DRVI6c 14 | 2ZeChHVZ/y6zjgnkDgrv2+NmrVSNWCYQzXHTJGsCmx0= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy/directory/file.txt: -------------------------------------------------------------------------------- 1 | wat 2 | -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy/file1.txt: -------------------------------------------------------------------------------- 1 | something happened 2 | -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy/file2.txt: -------------------------------------------------------------------------------- 1 | twice 2 | -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy2/.tsuruignore: -------------------------------------------------------------------------------- 1 | *.txt -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy2/directory/dir2/file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuru/tsuru-client/d2aaacd361307dcbbc0d4a838892714cfeaf4196/tsuru/client/testdata/deploy2/directory/dir2/file.txt -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy2/directory/file.txt: -------------------------------------------------------------------------------- 1 | wat 2 | -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy2/file1.txt: -------------------------------------------------------------------------------- 1 | something happened 2 | -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy2/file2.txt: -------------------------------------------------------------------------------- 1 | twice 2 | -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy3/.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy3/.tsuruignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuru/tsuru-client/d2aaacd361307dcbbc0d4a838892714cfeaf4196/tsuru/client/testdata/deploy3/.tsuruignore -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy4/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox:1.36.0-glibc 2 | 3 | COPY ./app.sh /usr/local/bin/ 4 | -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy4/app.sh: -------------------------------------------------------------------------------- 1 | echo "Starting my application :P" 2 | -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy5/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox:1.36.0-glibc 2 | 3 | COPY ./job.sh /usr/local/bin/ 4 | -------------------------------------------------------------------------------- /tsuru/client/testdata/deploy5/job.sh: -------------------------------------------------------------------------------- 1 | echo "My job here is done!" 2 | -------------------------------------------------------------------------------- /tsuru/config/diff/diff.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package diff 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "os" 12 | "os/exec" 13 | "strings" 14 | ) 15 | 16 | var ( 17 | stdin io.ReadWriter = os.Stdin 18 | ) 19 | 20 | // Returns diff of two io.Reader using cmd diff tool 21 | func Diff(current, newer io.Reader) ([]byte, error) { 22 | f1, err := writeTempFile(current) 23 | if err != nil { 24 | return nil, err 25 | } 26 | defer os.Remove(f1) 27 | 28 | f2, err := writeTempFile(newer) 29 | if err != nil { 30 | return nil, err 31 | } 32 | defer os.Remove(f2) 33 | 34 | data, err := exec.Command("diff", "-U0", "--label=current", f1, "--label=newer", f2).CombinedOutput() 35 | if len(data) > 0 { 36 | // diff exits with a non-zero status when the files don't match. 37 | // Ignore that failure as long as we get output. 38 | err = nil 39 | } 40 | 41 | return data, err 42 | } 43 | 44 | func ReplaceWithSudo(originalFile string, newerContent io.Reader) error { 45 | f1, err := writeTempFile(newerContent) 46 | if err != nil { 47 | return err 48 | } 49 | defer os.Remove(f1) 50 | 51 | output1, err1 := exec.Command("cp", f1, originalFile).CombinedOutput() 52 | if err1 != nil { 53 | // try with sudo 54 | cmd := exec.Command("sudo", "cp", f1, originalFile) 55 | localStderr := &bytes.Buffer{} 56 | cmd.Stderr = localStderr 57 | cmd.Stdin = stdin // handles password input 58 | err2 := cmd.Run() 59 | if err2 != nil { 60 | e, err3 := io.ReadAll(localStderr) 61 | return fmt.Errorf( 62 | `could not "cp" the current file, even with sudo: %s (%s) | %s (%s%s)`, 63 | err1, string(output1), err2, strings.TrimSpace(string(e)), errOrEmpty(err3), 64 | ) 65 | } 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func writeTempFile(data io.Reader) (string, error) { 72 | file, err := os.CreateTemp("", "") 73 | if err != nil { 74 | return "", err 75 | } 76 | bytes, err := io.ReadAll(data) 77 | if err != nil { 78 | return "", err 79 | } 80 | _, err = file.Write(bytes) 81 | if err1 := file.Close(); err == nil { 82 | err = err1 83 | } 84 | if err != nil { 85 | os.Remove(file.Name()) 86 | return "", err 87 | } 88 | return file.Name(), nil 89 | } 90 | 91 | func errOrEmpty(e error) string { 92 | if e == nil { 93 | return "" 94 | } 95 | return e.Error() 96 | } 97 | -------------------------------------------------------------------------------- /tsuru/config/diff/diff_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package diff 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | "testing" 11 | 12 | "gopkg.in/check.v1" 13 | ) 14 | 15 | type S struct{} 16 | 17 | var _ = check.Suite(&S{}) 18 | 19 | func Test(t *testing.T) { check.TestingT(t) } 20 | 21 | func (s *S) TestDiff(c *check.C) { 22 | current := `# This is a file 23 | this line will be kept 24 | this line will be altered 25 | ` 26 | newer := `# This is a file 27 | this line will be kept 28 | this line was altered 29 | ` 30 | expected := `--- current 31 | +++ newer 32 | @@ -3 +3 @@ 33 | -this line will be altered 34 | +this line was altered 35 | ` 36 | result, err := Diff(strings.NewReader(current), strings.NewReader(newer)) 37 | c.Assert(err, check.IsNil) 38 | c.Assert(string(result), check.Equals, expected) 39 | 40 | result, err = Diff(strings.NewReader("no changes"), strings.NewReader("no changes")) 41 | c.Assert(err, check.IsNil) 42 | c.Assert(string(result), check.Equals, "") 43 | } 44 | 45 | func (s *S) TestErrOrEmpty(c *check.C) { 46 | c.Assert(errOrEmpty(nil), check.Equals, "") 47 | c.Assert(errOrEmpty(fmt.Errorf("This is a new error")), check.Equals, "This is a new error") 48 | } 49 | -------------------------------------------------------------------------------- /tsuru/config/selfupdater/suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package selfupdater 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | 11 | "github.com/tsuru/tsuru/fs" 12 | "gopkg.in/check.v1" 13 | ) 14 | 15 | type S struct{} 16 | 17 | var _ = check.Suite(&S{}) 18 | 19 | func Test(t *testing.T) { check.TestingT(t) } 20 | 21 | func (s *S) SetUpTest(c *check.C) { 22 | nowUTC = func() time.Time { return time.Now().UTC() } // so we can test time-dependent features 23 | fsystem = fs.OsFs{} 24 | } 25 | -------------------------------------------------------------------------------- /tsuru/config/selfupdater/uptodate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package selfupdater 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/Masterminds/semver/v3" 18 | "github.com/tsuru/go-tsuruclient/pkg/config" 19 | ) 20 | 21 | const ( 22 | defaultSnoozeByDuration time.Duration = 0 * time.Hour 23 | DefaultLatestManifestURL string = "https://github.com/tsuru/tsuru-client/releases/latest/download/metadata.json" 24 | ) 25 | 26 | var ( 27 | stderr io.ReadWriter = os.Stderr 28 | nowUTC func() time.Time = func() time.Time { return time.Now().UTC() } // so we can test time-dependent features 29 | snoozeDuration time.Duration 30 | forceCheckAfterDuration time.Duration = 3 * 30 * 24 * time.Hour 31 | overrideForceCheck *bool 32 | ) 33 | 34 | func init() { 35 | if snoozeDurationStr := os.Getenv("TSURU_CLIENT_SELF_UPDATE_SNOOZE_DURATION"); snoozeDurationStr != "" { 36 | if duration, err := time.ParseDuration(snoozeDurationStr); err == nil { 37 | snoozeDuration = duration 38 | } else { 39 | fmt.Fprintln(stderr, "WARN: when setting TSURU_CLIENT_SELF_UPDATE_SNOOZE_DURATION, it must be a parsable duration (eg: 10m, 72h, etc...)") 40 | } 41 | } 42 | 43 | if forceCheckStr := os.Getenv("TSURU_CLIENT_FORCE_CHECK_UPDATES"); forceCheckStr != "" { 44 | if isForceCheck, err := strconv.ParseBool(forceCheckStr); err == nil { 45 | overrideForceCheck = &isForceCheck 46 | } else { 47 | fmt.Fprintln(stderr, "WARN: when setting TSURU_CLIENT_FORCE_CHECK_UPDATES, it must be either true or false") 48 | } 49 | } 50 | } 51 | 52 | type latestVersionCheckResult struct { 53 | isFinished bool 54 | isOutdated bool 55 | latestVersion string 56 | err error 57 | } 58 | 59 | type latestVersionCheck struct { 60 | currentVersion string 61 | forceCheckBeforeFinish bool 62 | result chan latestVersionCheckResult 63 | } 64 | 65 | type releaseMetadata struct { 66 | Version string `json:"version"` 67 | Date time.Time `json:"date"` 68 | } 69 | 70 | // This function "returns" its results over the r.result channel 71 | func getRemoteVersionAndReportsToChanGoroutine(r *latestVersionCheck) { 72 | conf := config.GetConfig() 73 | checkResult := latestVersionCheckResult{ 74 | isFinished: true, 75 | latestVersion: r.currentVersion, 76 | } 77 | 78 | if strings.HasPrefix(r.currentVersion, "dev") || conf.ClientSelfUpdater.LastCheck.Add(snoozeDuration).After(nowUTC()) { 79 | r.result <- checkResult 80 | return 81 | } 82 | 83 | response, err := http.Get(conf.ClientSelfUpdater.LatestManifestURL) 84 | if err != nil { 85 | checkResult.err = fmt.Errorf("could not GET endpoint %q: %w", conf.ClientSelfUpdater.LatestManifestURL, err) 86 | r.result <- checkResult 87 | return 88 | } 89 | defer response.Body.Close() 90 | if response.StatusCode > 300 { 91 | checkResult.err = fmt.Errorf("could not GET endpoint %q: %v", conf.ClientSelfUpdater.LatestManifestURL, response.Status) 92 | r.result <- checkResult 93 | return 94 | } 95 | 96 | data, err := io.ReadAll(response.Body) 97 | if err != nil { 98 | checkResult.err = fmt.Errorf("could not read response body: %w", err) 99 | r.result <- checkResult 100 | return 101 | } 102 | 103 | var metadata releaseMetadata 104 | err = json.Unmarshal(data, &metadata) 105 | if err != nil { 106 | checkResult.err = fmt.Errorf("could not parse metadata.json. Unexpected format: %w", err) 107 | r.result <- checkResult 108 | return 109 | } 110 | 111 | current, err := semver.NewVersion(r.currentVersion) 112 | if err != nil { 113 | current, _ = semver.NewVersion("0.0.0") 114 | } 115 | latest, err := semver.NewVersion(metadata.Version) 116 | if err != nil { 117 | checkResult.err = fmt.Errorf("metadata.version is not a SemVersion: %w\nmetadata: %v (parsed from %q)", err, metadata, string(data)) 118 | r.result <- checkResult 119 | return 120 | } 121 | 122 | conf.ClientSelfUpdater.LastCheck = nowUTC() 123 | if current.Compare(latest) < 0 { 124 | checkResult.latestVersion = latest.String() 125 | checkResult.isOutdated = true 126 | } 127 | r.result <- checkResult 128 | } 129 | 130 | func CheckLatestVersionBackground(currentVersion string) *latestVersionCheck { 131 | conf := config.GetConfig() 132 | 133 | forceCheckBeforeFinish := false 134 | if !conf.ClientSelfUpdater.LastCheck.IsZero() || overrideForceCheck != nil { // do not force on empty config.ClientSelfUpdater 135 | forceCheckBeforeFinish = conf.ClientSelfUpdater.LastCheck.Add(forceCheckAfterDuration).Before(nowUTC()) 136 | if overrideForceCheck != nil { 137 | forceCheckBeforeFinish = *overrideForceCheck 138 | } 139 | } 140 | 141 | r := &latestVersionCheck{ 142 | currentVersion: currentVersion, 143 | forceCheckBeforeFinish: forceCheckBeforeFinish, 144 | } 145 | r.result = make(chan latestVersionCheckResult, 1) 146 | go getRemoteVersionAndReportsToChanGoroutine(r) 147 | return r 148 | } 149 | 150 | func VerifyLatestVersion(lvCheck *latestVersionCheck) { 151 | checkResult := latestVersionCheckResult{} 152 | if lvCheck.forceCheckBeforeFinish { 153 | // blocking 154 | timeout := 2 * time.Second 155 | for !checkResult.isFinished { 156 | select { 157 | case <-time.After(timeout): 158 | fmt.Fprintln(stderr, "WARN: Taking too long to check for latest version. CTRL+C to force exit.") 159 | case checkResult = <-lvCheck.result: 160 | break 161 | } 162 | timeout += 2 * time.Second 163 | } 164 | 165 | } else { 166 | // non-blocking 167 | select { 168 | case checkResult = <-lvCheck.result: 169 | default: 170 | } 171 | } 172 | 173 | if checkResult.err != nil { 174 | fmt.Fprintf(stderr, "\n\nERROR: Could not query for latest version: %v\n", checkResult.err) 175 | } 176 | if checkResult.isFinished && checkResult.isOutdated { 177 | fmt.Fprintf(stderr, "\n\nINFO: A new version is available. Please update to the newer version %q (current: %q)\n", checkResult.latestVersion, lvCheck.currentVersion) 178 | if err := CheckPackageCloudRepo(); err != nil { 179 | fmt.Fprintf(stderr, "Got error after detecting an outdated package manager configuration: %v\n", err) 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /tsuru/formatter/date.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package formatter 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | ) 11 | 12 | var LocalTZ = time.Local 13 | 14 | func Local(date time.Time) time.Time { 15 | return date.In(LocalTZ) 16 | } 17 | 18 | func FormatStamp(date time.Time) string { 19 | return date.In(LocalTZ).Format(time.Stamp) 20 | } 21 | 22 | func FormatDate(date time.Time) string { 23 | if date.IsZero() { 24 | return "-" 25 | } 26 | return date.In(LocalTZ).Format(time.RFC822) 27 | } 28 | 29 | func FormatDuration(duration *time.Duration) string { 30 | if duration == nil { 31 | return "…" 32 | } 33 | 34 | seconds := *duration / time.Second 35 | minutes := seconds / 60 36 | seconds = seconds % 60 37 | return fmt.Sprintf("%02d:%02d", minutes, seconds) 38 | } 39 | 40 | func FormatDateAndDuration(date time.Time, duration *time.Duration) string { 41 | return fmt.Sprintf("%s (%s)", FormatDate(date), FormatDuration(duration)) 42 | } 43 | -------------------------------------------------------------------------------- /tsuru/formatter/date_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package formatter 6 | 7 | import ( 8 | "time" 9 | 10 | check "gopkg.in/check.v1" 11 | ) 12 | 13 | func (s *S) TestFormatDate(c *check.C) { 14 | parsedTs, err := time.Parse(time.RFC3339, "2018-02-16T11:03:00.000Z") 15 | c.Assert(err, check.IsNil) 16 | 17 | c.Assert(FormatDate(parsedTs), check.Equals, "16 Feb 18 05:03 CST") 18 | c.Assert(FormatDate(time.Time{}), check.Equals, "-") 19 | } 20 | 21 | func (s *S) TestFormatDuration(c *check.C) { 22 | duration := 75 * time.Second 23 | 24 | c.Assert(FormatDuration(&duration), check.Equals, "01:15") 25 | c.Assert(FormatDuration(nil), check.Equals, "…") 26 | } 27 | 28 | func (s *S) TestFormatDateAndDuration(c *check.C) { 29 | parsedTs, err := time.Parse(time.RFC3339, "2018-02-16T11:03:00.000Z") 30 | c.Assert(err, check.IsNil) 31 | duration := 123 * time.Second 32 | 33 | c.Assert(FormatDateAndDuration(parsedTs, &duration), check.Equals, "16 Feb 18 05:03 CST (02:03)") 34 | c.Assert(FormatDateAndDuration(parsedTs, nil), check.Equals, "16 Feb 18 05:03 CST (…)") 35 | } 36 | -------------------------------------------------------------------------------- /tsuru/formatter/json.go: -------------------------------------------------------------------------------- 1 | package formatter 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | func JSON(writer io.Writer, data interface{}) error { 9 | enc := json.NewEncoder(writer) 10 | enc.SetIndent("", " ") 11 | return enc.Encode(data) 12 | } 13 | -------------------------------------------------------------------------------- /tsuru/formatter/stream.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package formatter 6 | 7 | import ( 8 | "io" 9 | "net/http" 10 | 11 | "github.com/pkg/errors" 12 | tsuruIO "github.com/tsuru/tsuru/io" 13 | ) 14 | 15 | // StreamJSONResponse supports the JSON streaming format from the tsuru API. 16 | func StreamJSONResponse(w io.Writer, response *http.Response) error { 17 | if response == nil { 18 | return errors.New("response cannot be nil") 19 | } 20 | defer response.Body.Close() 21 | var err error 22 | output := tsuruIO.NewStreamWriter(w, nil) 23 | for n := int64(1); n > 0 && err == nil; n, err = io.Copy(output, response.Body) { 24 | } 25 | if err != nil { 26 | return err 27 | } 28 | unparsed := output.Remaining() 29 | if len(unparsed) > 0 { 30 | return errors.Errorf("unparsed message error: %s", string(unparsed)) 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /tsuru/formatter/suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 tsuru-client authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package formatter 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | 11 | "gopkg.in/check.v1" 12 | ) 13 | 14 | type S struct { 15 | defaultLocation time.Location 16 | } 17 | 18 | var _ = check.Suite(&S{}) 19 | 20 | func Test(t *testing.T) { check.TestingT(t) } 21 | 22 | func (s *S) SetUpTest(c *check.C) { 23 | s.defaultLocation = *LocalTZ 24 | location, err := time.LoadLocation("US/Central") 25 | if err == nil { 26 | LocalTZ = location 27 | } 28 | } 29 | 30 | func (s *S) TearDownTest(c *check.C) { 31 | LocalTZ = &s.defaultLocation 32 | } 33 | -------------------------------------------------------------------------------- /tsuru/http/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package http 6 | 7 | import ( 8 | "io" 9 | "net/http" 10 | "net/url" 11 | 12 | "github.com/tsuru/go-tsuruclient/pkg/config" 13 | "github.com/tsuru/go-tsuruclient/pkg/tsuru" 14 | ) 15 | 16 | var ( 17 | AuthenticatedClient = &http.Client{} 18 | UnauthenticatedClient = &http.Client{} 19 | ) 20 | 21 | var ( 22 | TerminalClientOnlyRequest = 1 23 | TerminalClientVerbose = 2 24 | ) 25 | 26 | type TerminalClientOptions struct { 27 | RoundTripper http.RoundTripper 28 | ClientName string 29 | ClientVersion string 30 | Stdout io.Writer 31 | Stderr io.Writer 32 | } 33 | 34 | func NewTerminalClient(opts TerminalClientOptions) *http.Client { 35 | stdout := io.Discard 36 | stderr := io.Discard 37 | 38 | if opts.Stdout != nil { 39 | stdout = opts.Stdout 40 | } 41 | 42 | if opts.Stderr != nil { 43 | stderr = opts.Stderr 44 | } 45 | 46 | transport := &TerminalRoundTripper{ 47 | RoundTripper: opts.RoundTripper, 48 | Stdout: stdout, 49 | Stderr: stderr, 50 | Progname: opts.ClientName, 51 | CurrentVersion: opts.ClientVersion, 52 | } 53 | return &http.Client{Transport: transport} 54 | } 55 | 56 | func TsuruClientFromEnvironment() (*tsuru.APIClient, error) { 57 | cfg := &tsuru.Configuration{ 58 | HTTPClient: AuthenticatedClient, 59 | DefaultHeader: map[string]string{}, 60 | } 61 | 62 | var err error 63 | cfg.BasePath, err = config.GetTarget() 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | cli := tsuru.NewAPIClient(cfg) 69 | return cli, nil 70 | } 71 | 72 | type errWrapped interface { 73 | Unwrap() error 74 | } 75 | 76 | type errCauser interface { 77 | Cause() error 78 | } 79 | 80 | func UnwrapErr(err error) error { 81 | for err != nil { 82 | if cause, ok := err.(errCauser); ok { 83 | err = cause.Cause() 84 | } else if u, ok := err.(errWrapped); ok { 85 | err = u.Unwrap() 86 | } else if urlErr, ok := err.(*url.Error); ok { 87 | err = urlErr.Err 88 | } else { 89 | break 90 | } 91 | } 92 | 93 | return err 94 | } 95 | -------------------------------------------------------------------------------- /tsuru/http/transport.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 tsuru authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package http 6 | 7 | import ( 8 | "crypto/x509" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "net/http/httputil" 13 | "os" 14 | "strconv" 15 | 16 | goVersion "github.com/hashicorp/go-version" 17 | "github.com/pkg/errors" 18 | "github.com/tsuru/go-tsuruclient/pkg/config" 19 | tsuruerr "github.com/tsuru/tsuru/errors" 20 | ) 21 | 22 | var ( 23 | _ http.RoundTripper = &TerminalRoundTripper{} 24 | defaultRoundTripper = http.DefaultTransport 25 | ) 26 | 27 | const ( 28 | versionHeader = "Supported-Tsuru" 29 | verbosityHeader = "X-Tsuru-Verbosity" 30 | 31 | invalidVersionFormat = `##################################################################### 32 | 33 | WARNING: You're using an unsupported version of %s. 34 | 35 | You must have at least version %s, your current 36 | version is %s. 37 | 38 | Please go to http://docs.tsuru.io/en/latest/using/install-client.html 39 | and download the last version. 40 | 41 | ##################################################################### 42 | 43 | ` 44 | ) 45 | 46 | var errUnauthorized = &tsuruerr.HTTP{Code: http.StatusUnauthorized, Message: "unauthorized"} 47 | 48 | // TerminalRoundTripper is a RoundTripper that dumps request and response 49 | // based on the Verbosity. 50 | // Verbosity >= 1 --> Dumps request 51 | // Verbosity >= 2 --> Dumps response 52 | type TerminalRoundTripper struct { 53 | http.RoundTripper 54 | Stdout io.Writer 55 | Stderr io.Writer 56 | CurrentVersion string 57 | Progname string 58 | } 59 | 60 | func getVerbosity() int { 61 | v, _ := strconv.Atoi(os.Getenv("TSURU_VERBOSITY")) 62 | return v 63 | } 64 | 65 | func (v *TerminalRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 66 | roundTripper := v.RoundTripper 67 | if roundTripper == nil { 68 | roundTripper = defaultRoundTripper 69 | } 70 | verbosity := getVerbosity() 71 | req.Header.Add(verbosityHeader, strconv.Itoa(verbosity)) 72 | req.Header.Set("User-Agent", fmt.Sprintf("tsuru-client/%s", v.CurrentVersion)) 73 | req.Close = true 74 | 75 | if verbosity >= TerminalClientOnlyRequest { 76 | fmt.Fprintf(v.Stdout, "*************************** **********************************\n", req.URL.RequestURI()) 77 | requestDump, err := httputil.DumpRequest(req, true) 78 | if err != nil { 79 | return nil, err 80 | } 81 | fmt.Fprint(v.Stdout, string(requestDump)) 82 | if requestDump[len(requestDump)-1] != '\n' { 83 | fmt.Fprintln(v.Stdout) 84 | } 85 | fmt.Fprintf(v.Stdout, "*************************** **********************************\n", req.URL.RequestURI()) 86 | } 87 | 88 | response, err := roundTripper.RoundTrip(req) 89 | if verbosity >= TerminalClientVerbose && response != nil { 90 | fmt.Fprintf(v.Stdout, "*************************** **********************************\n", req.URL.RequestURI()) 91 | responseDump, errDump := httputil.DumpResponse(response, true) 92 | if errDump != nil { 93 | return nil, errDump 94 | } 95 | fmt.Fprint(v.Stdout, string(responseDump)) 96 | if responseDump[len(responseDump)-1] != '\n' { 97 | fmt.Fprintln(v.Stdout) 98 | } 99 | fmt.Fprintf(v.Stdout, "*************************** **********************************\n", req.URL.RequestURI()) 100 | } 101 | err = detectClientError(err) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | supported := response.Header.Get(versionHeader) 107 | if !validateVersion(supported, v.CurrentVersion) { 108 | fmt.Fprintf(v.Stderr, invalidVersionFormat, v.Progname, supported, v.CurrentVersion) 109 | } 110 | 111 | if response.StatusCode > 399 { 112 | err := &tsuruerr.HTTP{ 113 | Code: response.StatusCode, 114 | Message: response.Status, 115 | } 116 | 117 | defer response.Body.Close() 118 | body, _ := io.ReadAll(response.Body) 119 | if len(body) > 0 { 120 | err.Message = string(body) 121 | } 122 | 123 | return nil, err 124 | } 125 | 126 | return response, err 127 | } 128 | 129 | func detectClientError(err error) error { 130 | if err == nil { 131 | return nil 132 | } 133 | detectErr := func(e error) error { 134 | target, _ := config.ReadTarget() 135 | 136 | switch e := e.(type) { 137 | case *tsuruerr.HTTP: 138 | return errors.Wrapf(e, "Error received from tsuru server (%s), %d", target, e.Code) 139 | case x509.UnknownAuthorityError: 140 | return errors.Wrapf(e, "Failed to connect to tsuru server (%s)", target) 141 | } 142 | return errors.Wrapf(e, "Failed to connect to tsuru server (%s), it's probably down", target) 143 | } 144 | 145 | return detectErr(UnwrapErr(err)) 146 | } 147 | 148 | // validateVersion checks whether current version is greater or equal to 149 | // supported version. 150 | func validateVersion(supported, current string) bool { 151 | if current == "dev" { 152 | return true 153 | } 154 | if supported == "" { 155 | return true 156 | } 157 | vSupported, err := goVersion.NewVersion(supported) 158 | if err != nil { 159 | return false 160 | } 161 | vCurrent, err := goVersion.NewVersion(current) 162 | if err != nil { 163 | return false 164 | } 165 | return vCurrent.Compare(vSupported) >= 0 166 | } 167 | 168 | type TokenV1RoundTripper struct { 169 | http.RoundTripper 170 | } 171 | 172 | func (v *TokenV1RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 173 | roundTripper := v.RoundTripper 174 | if roundTripper == nil { 175 | roundTripper = defaultRoundTripper 176 | } 177 | 178 | if token, err := config.ReadTokenV1(); err == nil && token != "" { 179 | req.Header.Set("Authorization", "bearer "+token) 180 | } 181 | 182 | response, err := roundTripper.RoundTrip(req) 183 | 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | if response.StatusCode == http.StatusUnauthorized { 189 | if teamToken := config.ReadTeamToken(); teamToken != "" { 190 | fmt.Fprintln(os.Stderr, "Invalid session - maybe invalid defined token on TSURU_TOKEN envvar") 191 | } else { 192 | fmt.Fprintln(os.Stderr, "Invalid session") 193 | } 194 | 195 | return nil, errUnauthorized 196 | } 197 | 198 | return response, nil 199 | } 200 | 201 | func NewTokenV1RoundTripper() http.RoundTripper { 202 | return &TokenV1RoundTripper{ 203 | RoundTripper: defaultRoundTripper, 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /tsuru/http/transport_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/tsuru/tsuru/cmd/cmdtest" 9 | check "gopkg.in/check.v1" 10 | ) 11 | 12 | func (s *S) TestVerboseRoundTripperDumpRequest(c *check.C) { 13 | os.Setenv("TSURU_VERBOSITY", "1") 14 | defer func() { 15 | os.Unsetenv("TSURU_VERBOSITY") 16 | }() 17 | out := new(bytes.Buffer) 18 | r := TerminalRoundTripper{ 19 | Stdout: out, 20 | CurrentVersion: "1.0.0", 21 | RoundTripper: &cmdtest.Transport{ 22 | Message: "Success!", 23 | Status: http.StatusOK, 24 | }, 25 | } 26 | req, err := http.NewRequest(http.MethodGet, "http://localhost/users", nil) 27 | c.Assert(err, check.IsNil) 28 | _, err = r.RoundTrip(req) 29 | c.Assert(err, check.IsNil) 30 | c.Assert(out.String(), check.DeepEquals, "*************************** **********************************\n"+ 31 | "GET /users HTTP/1.1\r\n"+ 32 | "Host: localhost\r\n"+ 33 | "User-Agent: tsuru-client/1.0.0\r\n"+ 34 | "X-Tsuru-Verbosity: 1\r\n"+ 35 | "\r\n"+ 36 | "*************************** **********************************\n") 37 | } 38 | 39 | func (s *S) TestVerboseRoundTripperDumpRequestResponse2(c *check.C) { 40 | os.Setenv("TSURU_VERBOSITY", "2") 41 | defer func() { 42 | os.Unsetenv("TSURU_VERBOSITY") 43 | }() 44 | 45 | out := new(bytes.Buffer) 46 | r := TerminalRoundTripper{ 47 | Stdout: out, 48 | CurrentVersion: "1.2.0", 49 | RoundTripper: &cmdtest.Transport{ 50 | Message: "Success!", 51 | Status: http.StatusOK, 52 | }, 53 | } 54 | req, err := http.NewRequest(http.MethodGet, "http://localhost/users", nil) 55 | c.Assert(err, check.IsNil) 56 | _, err = r.RoundTrip(req) 57 | c.Assert(err, check.IsNil) 58 | c.Assert(out.String(), check.DeepEquals, "*************************** **********************************\n"+ 59 | "GET /users HTTP/1.1\r\n"+ 60 | "Host: localhost\r\n"+ 61 | "User-Agent: tsuru-client/1.2.0\r\n"+ 62 | "X-Tsuru-Verbosity: 2\r\n"+ 63 | "\r\n"+ 64 | "*************************** **********************************\n"+ 65 | "*************************** **********************************\n"+ 66 | "HTTP/0.0 200 OK\r\n"+ 67 | "\r\n"+ 68 | "Success!\n"+ 69 | "*************************** **********************************\n") 70 | 71 | } 72 | --------------------------------------------------------------------------------