├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── BUG_REPORT_AUTH.md │ ├── FEATURE_REQUEST.md │ └── QUESTION.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── check.go ├── completion.go ├── describe.go ├── describe_servers.go ├── describe_specs.go ├── describe_targets.go ├── describe_tasks.go ├── edit.go ├── edit_servers.go ├── edit_spec.go ├── edit_target.go ├── edit_task.go ├── exec.go ├── gen.go ├── gen_docs.go ├── init.go ├── list.go ├── list_servers.go ├── list_specs.go ├── list_tags.go ├── list_targets.go ├── list_tasks.go ├── root.go ├── run.go └── ssh.go ├── core ├── config.man ├── dao │ ├── common.go │ ├── common_test.go │ ├── config.go │ ├── import_config.go │ ├── import_task.go │ ├── server.go │ ├── server_test.go │ ├── spec.go │ ├── tag.go │ ├── target.go │ ├── task.go │ └── theme.go ├── errors.go ├── flags.go ├── hostname-gen.go ├── hostname-gen_test.go ├── man.go ├── man_gen.go ├── prefixer.go ├── print │ ├── lib.go │ ├── print_block.go │ ├── print_table.go │ ├── report.go │ └── table.go ├── run │ ├── client.go │ ├── exec.go │ ├── exec_test.go │ ├── localhost.go │ ├── ssh.go │ ├── table.go │ ├── text.go │ ├── unix.go │ └── windows.go ├── sake.1 ├── spinner.go ├── ssh_config.go ├── test │ └── utils.go ├── utils.go └── utils_test.go ├── debug ├── docs ├── ansible.md ├── background.md ├── changelog.md ├── command.md ├── config.md ├── contributing.md ├── development.md ├── error-handling.md ├── examples.md ├── installation.md ├── introduction.md ├── inventory.md ├── man-pages.md ├── output-format.md ├── performance.md ├── recipes.md ├── roadmap.md ├── shell-completion.mdx ├── task-execution.md ├── usage.md ├── variables.md └── work-dir.md ├── go.mod ├── go.sum ├── img ├── cpu-1.csv ├── cpu-1.png ├── cpu-2.png ├── dependency-graph.svg ├── docusaurus.png ├── favicon.ico ├── free-strategy.png ├── host_pinned-strategy.png ├── linear-strategy.png ├── linear-strategy.svg ├── logo.png ├── logo.svg ├── mem-1.csv ├── mem-1.png ├── mem-2.png ├── output.gif ├── time-1-short.png ├── time-1.csv ├── time-1.png ├── time-2-short.png ├── time-2.png ├── undraw_docusaurus_mountain.svg ├── undraw_docusaurus_react.svg └── undraw_docusaurus_tree.svg ├── install.sh ├── main.go ├── res ├── graph-full.svg ├── graph.svg ├── logo.png ├── logo.svg ├── output.gif └── output.json ├── scripts ├── gif.sh └── release.sh └── test ├── Dockerfile ├── README.md ├── authorized_keys ├── benchmark.sh ├── docker-compose-performance.yaml ├── docker-compose.yaml ├── integration ├── golden │ ├── golden-0.stdout │ ├── golden-1.stdout │ ├── golden-10.stdout │ ├── golden-11.stdout │ ├── golden-12.stdout │ ├── golden-13.stdout │ ├── golden-14.stdout │ ├── golden-15.stdout │ ├── golden-16.stdout │ ├── golden-17.stdout │ ├── golden-18.stdout │ ├── golden-19.stdout │ ├── golden-2.stdout │ ├── golden-20.stdout │ ├── golden-21.stdout │ ├── golden-22.stdout │ ├── golden-23.stdout │ ├── golden-24.stdout │ ├── golden-25.stdout │ ├── golden-26.stdout │ ├── golden-27.stdout │ ├── golden-28.stdout │ ├── golden-29.stdout │ ├── golden-3.stdout │ ├── golden-30.stdout │ ├── golden-31.stdout │ ├── golden-32.stdout │ ├── golden-33.stdout │ ├── golden-34.stdout │ ├── golden-35.stdout │ ├── golden-36.stdout │ ├── golden-37.stdout │ ├── golden-38.stdout │ ├── golden-39.stdout │ ├── golden-4.stdout │ ├── golden-40.stdout │ ├── golden-41.stdout │ ├── golden-42.stdout │ ├── golden-5.stdout │ ├── golden-6.stdout │ ├── golden-7.stdout │ ├── golden-8.stdout │ └── golden-9.stdout ├── main_test.go └── run_test.go ├── keys ├── id_ed25519_pem ├── id_ed25519_pem.pub ├── id_ed25519_pem_no ├── id_ed25519_pem_no.pub ├── id_ed25519_rfc ├── id_ed25519_rfc.pub ├── id_ed25519_rfc_no ├── id_ed25519_rfc_no.pub ├── id_rsa_pem ├── id_rsa_pem.pub ├── id_rsa_pem_no ├── id_rsa_pem_no.pub ├── id_rsa_rfc ├── id_rsa_rfc.pub ├── id_rsa_rfc_no └── id_rsa_rfc_no.pub ├── playground ├── env │ └── sake.yaml ├── import │ ├── imports │ │ ├── sake-1.yaml │ │ └── sake-2.yaml │ ├── sake.yaml │ └── script.sh ├── inventory.sh ├── list-of-servers.txt ├── misc │ ├── common │ │ └── common.yaml │ ├── roles │ │ ├── hello.txt │ │ └── tasks.yaml │ └── sake.yaml ├── nested │ └── sake.yaml ├── performance │ └── sake.yaml ├── sake.yaml ├── script.sh ├── spec │ └── sake.yaml ├── tasks │ └── tasks.yaml └── work-dir │ └── sake.yaml ├── profiles ├── list-servers ├── nested ├── nested-parallel ├── ping ├── ping-no-key └── ping-parallel ├── sake.yaml ├── servers.yaml ├── tasks.yaml └── user-config.yaml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://paypal.me/samiralajmovic", "https://www.buymeacoffee.com/alajmo"] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | - [ ] I have the latest version of sake 10 | - [ ] I have searched through the existing issues 11 | 12 | ## Info 13 | 14 | - OS 15 | - [ ] Linux 16 | - [ ] Mac OS X 17 | - [ ] Windows 18 | - [ ] other 19 | 20 | - Shell 21 | - [ ] Bash 22 | - [ ] Zsh 23 | - [ ] Fish 24 | - [ ] Powershell 25 | - [ ] other 26 | 27 | 28 | - Version: 29 | 30 | ## Problem / Steps to reproduce 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT_AUTH.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Auth Bug report 3 | about: Report an connection to server bug 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | - [ ] I have the latest version of sake 10 | - [ ] I have searched through the existing issues 11 | 12 | ## Info 13 | 14 | - OS 15 | - [ ] Linux 16 | - [ ] Mac OS X 17 | - [ ] Windows 18 | - [ ] other 19 | 20 | 21 | - Version: 22 | 23 | - Authentication: 24 | - [ ] Identity Key + Passphrase 25 | - [ ] Identity Key 26 | - [ ] Passphrase 27 | - [ ] SSH Agent 28 | 29 | - Key Type: 30 | - [ ] rsa 31 | - [ ] ed25519 32 | - [ ] other 33 | 34 | - Key Format: 35 | - [ ] rfc4716 36 | - [ ] pkcs8 37 | - [ ] pem 38 | 39 | ## Problem / Steps to reproduce 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an feature for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | --- 8 | 9 | ## Is your feature request related to a problem? Please describe 10 | 11 | ## Describe the solution you'd like 12 | 13 | ## Additional context 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/QUESTION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question 4 | title: '' 5 | labels: 'question' 6 | assignees: '' 7 | --- 8 | 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What's Changed 2 | 3 | A description of the issue/feature; reference the issue number (if one exists). The Pull Request should not include fixes for issues other than the main issue/feature request. 4 | 5 | ## Technical Description 6 | 7 | Any specific technical detail that may provide additional context to aid code review. 8 | 9 | > Before opening a Pull Request you should read and agreed to the Contributor Code of Conduct (see `CONTRIBUTING.md`\) 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths-ignore: 7 | - '**.md' 8 | jobs: 9 | test: 10 | name: test 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | go: ['1.20'] 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: '1.20' 25 | cache: false 26 | 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v3 29 | 30 | - name: Get dependencies 31 | run: go get -v -t -d ./... 32 | 33 | - name: Test 34 | run: make test 35 | 36 | - name: Build 37 | run: make build 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: '1.20' 20 | cache: false 21 | 22 | - name: Create release notes 23 | run: ./scripts/release.sh 24 | 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v3 27 | with: 28 | distribution: goreleaser 29 | version: latest 30 | args: release --release-notes=release-changelog.md 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.GH_PAT }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | dist 18 | target 19 | test/tmp 20 | release-changelog.md 21 | 22 | .netlify 23 | .tags 24 | known_hosts 25 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: sake 2 | 3 | before: 4 | hooks: 5 | - go mod download 6 | 7 | builds: 8 | - binary: sake 9 | id: sake 10 | ldflags: -s -w -X github.com/alajmo/sake/cmd.version={{ .Version }} -X github.com/alajmo/sake/cmd.commit={{ .ShortCommit }} -X github.com/alajmo/sake/cmd.date={{ .Date }} 11 | env: 12 | - CGO_ENABLED=0 13 | goos: 14 | - darwin 15 | - linux 16 | - windows 17 | - freebsd 18 | - netbsd 19 | - openbsd 20 | goarch: 21 | - amd64 22 | - 386 23 | - arm 24 | - arm64 25 | goarm: 26 | - 7 27 | 28 | ignore: 29 | - goos: freebsd 30 | goarch: arm 31 | - goos: freebsd 32 | goarch: arm64 33 | 34 | - goos: openbsd 35 | goarch: arm 36 | - goos: openbsd 37 | goarch: arm64 38 | 39 | - goos: darwin 40 | goarch: arm 41 | - goos: darwin 42 | goarch: 386 43 | 44 | - goos: windows 45 | goarch: arm 46 | - goos: windows 47 | goarch: arm64 48 | 49 | archives: 50 | - id: 'sake' 51 | builds: ['sake'] 52 | format: tar.gz 53 | format_overrides: 54 | - goos: windows 55 | format: zip 56 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 57 | files: 58 | - LICENSE 59 | - src: 'core/sake.1' 60 | dst: '.' 61 | strip_parent: true 62 | 63 | brews: 64 | - name: sake 65 | description: "sake is a CLI tool that enables you to run commands on servers via ssh" 66 | homepage: "https://sakecli.com" 67 | license: "MIT" 68 | tap: 69 | owner: alajmo 70 | name: homebrew-sake 71 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" 72 | folder: Formula 73 | 74 | checksum: 75 | name_template: 'checksums.txt' 76 | 77 | snapshot: 78 | name_template: '{{ .Tag }}' 79 | 80 | changelog: 81 | skip: true 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Samir Alajmovic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := sake 2 | PACKAGE := github.com/alajmo/$(NAME) 3 | DATE := $(shell date +%FT%T%Z) 4 | GIT := $(shell [ -d .git ] && git rev-parse --short HEAD) 5 | VERSION := v0.15.1 6 | 7 | default: build 8 | 9 | tidy: 10 | go get -u && go mod tidy 11 | 12 | gofmt: 13 | go fmt ./cmd/***.go 14 | go fmt ./core/***.go 15 | go fmt ./core/dao/***.go 16 | go fmt ./core/run/***.go 17 | go fmt ./core/print/***.go 18 | go fmt ./test/integration/***.go 19 | 20 | lint: 21 | golangci-lint run ./cmd/... ./core/... ./test/... 22 | deadcode . 23 | 24 | benchmark: 25 | cd test && ./benchmark.sh 26 | 27 | benchmark-save: 28 | cd test && ./benchmark.sh --save 29 | 30 | test: 31 | # Unit tests 32 | go test -v ./core/... 33 | 34 | # Integration tests 35 | cd ./test && docker-compose up -d 36 | go test -v ./test/integration/... -count=5 -clean 37 | cd ./test && docker-compose down 38 | 39 | unit-test: 40 | go test -v ./core/... 41 | 42 | integration-test: 43 | go test -v ./test/integration/... -clean 44 | 45 | update-golden-files: 46 | go test ./test/integration/... -update 47 | 48 | mock-ssh: 49 | cd ./test && docker-compose up 50 | 51 | mock-performance-ssh: 52 | cd ./test && docker-compose -f docker-compose-performance.yaml up 53 | 54 | build: 55 | CGO_ENABLED=0 go build \ 56 | -ldflags "-s -w -X '${PACKAGE}/cmd.version=${VERSION}' -X '${PACKAGE}/cmd.commit=${GIT}' -X '${PACKAGE}/cmd.date=${DATE}'" \ 57 | -a -tags netgo -o dist/${NAME} main.go 58 | 59 | build-all: 60 | goreleaser release --skip-publish --rm-dist --snapshot 61 | 62 | build-and-link: 63 | go build \ 64 | -ldflags "-w -X '${PACKAGE}/cmd.version=${VERSION}' -X '${PACKAGE}/cmd.commit=${GIT}' -X '${PACKAGE}/cmd.date=${DATE}'" \ 65 | -a -tags netgo -o dist/${NAME} main.go 66 | cp ./dist/sake ~/.local/bin/sake 67 | 68 | gen-man: 69 | go run -ldflags="-X 'github.com/alajmo/sake/cmd.buildMode=man' -X '${PACKAGE}/cmd.version=${VERSION}' -X '${PACKAGE}/cmd.commit=${GIT}' -X '${PACKAGE}/cmd.date=${DATE}'" ./main.go gen-docs 70 | 71 | release: 72 | git tag ${VERSION} && git push origin ${VERSION} 73 | 74 | clean: 75 | $(RM) -r dist target 76 | 77 | .PHONY: tidy gofmt lint benchmark benchmark-save test unit-test integration-test update-golden-files mock-ssh build build-all build-and-link gen-man release clean 78 | -------------------------------------------------------------------------------- /cmd/check.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/alajmo/sake/core" 9 | ) 10 | 11 | func checkCmd(configErr *error) *cobra.Command { 12 | cmd := cobra.Command{ 13 | Use: "check", 14 | Short: "Validate config", 15 | Long: `Validate config.`, 16 | Example: ` # Validate config 17 | sake check`, 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | if *configErr != nil { 21 | fmt.Printf("Found configuration errors:\n\n") 22 | core.Exit(*configErr) 23 | } 24 | 25 | fmt.Println("Config Valid") 26 | }, 27 | DisableAutoGenTag: true, 28 | } 29 | 30 | return &cmd 31 | } 32 | -------------------------------------------------------------------------------- /cmd/completion.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/alajmo/sake/core" 9 | ) 10 | 11 | func completionCmd() *cobra.Command { 12 | cmd := cobra.Command{ 13 | Use: "completion [bash|zsh|fish]", 14 | Short: "Generate completion script", 15 | Long: `To load completions: 16 | Bash: 17 | 18 | $ source <(sake completion bash) 19 | 20 | # To load completions for each session, execute once: 21 | # Linux: 22 | $ sake completion bash > /etc/bash_completion.d/sake 23 | # macOS: 24 | $ sake completion bash > /usr/local/etc/bash_completion.d/sake 25 | 26 | Zsh: 27 | 28 | # If shell completion is not already enabled in your environment, 29 | # you will need to enable it. You can execute the following once: 30 | 31 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc 32 | 33 | # To load completions for each session, execute once: 34 | $ sake completion zsh > "${fpath[1]}/_sake" 35 | 36 | # You will need to start a new shell for this setup to take effect. 37 | 38 | fish: 39 | 40 | $ sake completion fish | source 41 | 42 | # To load completions for each session, execute once: 43 | $ sake completion fish > ~/.config/fish/completions/sake.fish 44 | 45 | PowerShell: 46 | 47 | PS> mani completion powershell | Out-String | Invoke-Expression 48 | 49 | # To load completions for every new session, run: 50 | PS> mani completion powershell > mani.ps1 51 | # and source this file from your PowerShell profile. 52 | `, 53 | ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, 54 | Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), 55 | Run: generateCompletion, 56 | } 57 | 58 | return &cmd 59 | } 60 | 61 | func generateCompletion(cmd *cobra.Command, args []string) { 62 | switch args[0] { 63 | case "bash": 64 | err := cmd.Root().GenBashCompletion(os.Stdout) 65 | core.CheckIfError(err) 66 | case "zsh": 67 | err := cmd.Root().GenZshCompletion(os.Stdout) 68 | core.CheckIfError(err) 69 | case "fish": 70 | err := cmd.Root().GenFishCompletion(os.Stdout, true) 71 | core.CheckIfError(err) 72 | case "powershell": 73 | err := cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) 74 | core.CheckIfError(err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /cmd/describe.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core/dao" 7 | ) 8 | 9 | func describeCmd(config *dao.Config, configErr *error) *cobra.Command { 10 | cmd := cobra.Command{ 11 | Aliases: []string{"desc"}, 12 | Use: "describe ", 13 | Short: "Describe servers, tasks, specs and targets", 14 | Long: "Describe servers, tasks, specs and targets", 15 | Example: ` # Describe servers 16 | sake describe servers 17 | 18 | # Describe tasks 19 | sake describe tasks`, 20 | DisableAutoGenTag: true, 21 | } 22 | 23 | cmd.AddCommand( 24 | describeServersCmd(config, configErr), 25 | describeTasksCmd(config, configErr), 26 | describeTargetsCmd(config, configErr), 27 | describeSpecsCmd(config, configErr), 28 | ) 29 | 30 | return &cmd 31 | } 32 | -------------------------------------------------------------------------------- /cmd/describe_servers.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/alajmo/sake/core" 9 | "github.com/alajmo/sake/core/dao" 10 | "github.com/alajmo/sake/core/print" 11 | ) 12 | 13 | func describeServersCmd(config *dao.Config, configErr *error) *cobra.Command { 14 | var serverFlags core.ServerFlags 15 | 16 | cmd := cobra.Command{ 17 | Aliases: []string{"server", "serv", "sv"}, 18 | Use: "servers [servers]", 19 | Short: "Describe servers", 20 | Long: "Describe servers.", 21 | Example: ` # Describe all servers 22 | sake describe servers 23 | 24 | # Describe servers that have tag 25 | sake describe servers --tags `, 26 | 27 | Run: func(cmd *cobra.Command, args []string) { 28 | core.CheckIfError(*configErr) 29 | describeServers(config, args, serverFlags) 30 | }, 31 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 32 | if *configErr != nil { 33 | return []string{}, cobra.ShellCompDirectiveDefault 34 | } 35 | 36 | values := config.GetServerNameAndDesc() 37 | return values, cobra.ShellCompDirectiveNoFileComp 38 | }, 39 | } 40 | cmd.Flags().SortFlags = false 41 | 42 | cmd.Flags().StringSliceVarP(&serverFlags.Tags, "tags", "t", []string{}, "filter servers by their tag") 43 | err := cmd.RegisterFlagCompletionFunc("tags", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 44 | if *configErr != nil { 45 | return []string{}, cobra.ShellCompDirectiveDefault 46 | } 47 | 48 | options := config.GetTags() 49 | return options, cobra.ShellCompDirectiveDefault 50 | }) 51 | core.CheckIfError(err) 52 | 53 | cmd.Flags().StringVarP(&serverFlags.Regex, "regex", "r", "", "filter servers on host regex") 54 | cmd.Flags().BoolVarP(&serverFlags.Invert, "invert", "v", false, "invert matching on servers") 55 | cmd.Flags().BoolVarP(&serverFlags.Edit, "edit", "e", false, "edit server") 56 | 57 | return &cmd 58 | } 59 | 60 | func describeServers( 61 | config *dao.Config, 62 | args []string, 63 | serverFlags core.ServerFlags, 64 | ) { 65 | var userArgs []string 66 | var serverArgs []string 67 | // Separate user arguments from task ids 68 | for _, arg := range args { 69 | if strings.Contains(arg, "=") { 70 | userArgs = append(userArgs, arg) 71 | } else { 72 | serverArgs = append(serverArgs, arg) 73 | } 74 | } 75 | 76 | if serverFlags.Edit { 77 | if len(serverArgs) > 0 { 78 | err := config.EditServer(serverArgs[0]) 79 | core.CheckIfError(err) 80 | } else { 81 | err := config.EditServer("") 82 | core.CheckIfError(err) 83 | } 84 | } else { 85 | allServers := false 86 | if len(serverArgs) == 0 && 87 | len(serverFlags.Tags) == 0 { 88 | allServers = true 89 | } 90 | 91 | err := config.ParseInventory(userArgs) 92 | core.CheckIfError(err) 93 | 94 | servers, err := config.FilterServers(allServers, serverArgs, serverFlags.Tags, serverFlags.Regex, serverFlags.Invert) 95 | core.CheckIfError(err) 96 | 97 | if len(servers) > 0 { 98 | print.PrintServerBlocks(servers) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cmd/describe_specs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | "github.com/alajmo/sake/core/print" 9 | ) 10 | 11 | func describeSpecsCmd(config *dao.Config, configErr *error) *cobra.Command { 12 | var specFlags core.SpecFlags 13 | 14 | cmd := cobra.Command{ 15 | Aliases: []string{"spec"}, 16 | Use: "specs [specs]", 17 | Short: "Describe specs", 18 | Long: "Describe specs.", 19 | Example: ` # Describe all specs 20 | sake describe specs`, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | core.CheckIfError(*configErr) 23 | describeSpecs(config, args, &specFlags) 24 | }, 25 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 26 | if *configErr != nil { 27 | return []string{}, cobra.ShellCompDirectiveDefault 28 | } 29 | 30 | specs := config.GetSpecNames() 31 | return specs, cobra.ShellCompDirectiveNoFileComp 32 | }, 33 | DisableAutoGenTag: true, 34 | } 35 | cmd.Flags().SortFlags = false 36 | 37 | cmd.Flags().BoolVarP(&specFlags.Edit, "edit", "e", false, "edit spec") 38 | 39 | return &cmd 40 | } 41 | 42 | func describeSpecs( 43 | config *dao.Config, 44 | args []string, 45 | specFlags *core.SpecFlags, 46 | ) { 47 | if specFlags.Edit { 48 | if len(args) > 0 { 49 | err := config.EditSpec(args[0]) 50 | core.CheckIfError(err) 51 | } else { 52 | err := config.EditSpec("") 53 | core.CheckIfError(err) 54 | } 55 | } 56 | 57 | var specs []dao.Spec 58 | if len(args) > 0 { 59 | t, err := config.GetSpecsByName(args) 60 | core.CheckIfError(err) 61 | specs = t 62 | } else { 63 | specs = config.Specs 64 | } 65 | 66 | print.PrintSpecBlocks(specs, false) 67 | } 68 | -------------------------------------------------------------------------------- /cmd/describe_targets.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | "github.com/alajmo/sake/core/print" 9 | ) 10 | 11 | func describeTargetsCmd(config *dao.Config, configErr *error) *cobra.Command { 12 | var targetFlags core.TargetFlags 13 | 14 | cmd := cobra.Command{ 15 | Aliases: []string{"target"}, 16 | Use: "targets [targets]", 17 | Short: "Describe targets", 18 | Long: "Describe targets.", 19 | Example: ` # Describe all targets 20 | sake describe targets`, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | core.CheckIfError(*configErr) 23 | describeTargets(config, args, &targetFlags) 24 | }, 25 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 26 | if *configErr != nil { 27 | return []string{}, cobra.ShellCompDirectiveDefault 28 | } 29 | 30 | targets := config.GetTargetNames() 31 | return targets, cobra.ShellCompDirectiveNoFileComp 32 | }, 33 | DisableAutoGenTag: true, 34 | } 35 | cmd.Flags().SortFlags = false 36 | 37 | cmd.Flags().BoolVarP(&targetFlags.Edit, "edit", "e", false, "edit target") 38 | 39 | return &cmd 40 | } 41 | 42 | func describeTargets( 43 | config *dao.Config, 44 | args []string, 45 | targetFlags *core.TargetFlags, 46 | ) { 47 | if targetFlags.Edit { 48 | if len(args) > 0 { 49 | err := config.EditTarget(args[0]) 50 | core.CheckIfError(err) 51 | } else { 52 | err := config.EditTarget("") 53 | core.CheckIfError(err) 54 | } 55 | } 56 | 57 | var targets []dao.Target 58 | if len(args) > 0 { 59 | t, err := config.GetTargetsByName(args) 60 | core.CheckIfError(err) 61 | targets = t 62 | } else { 63 | targets = config.Targets 64 | } 65 | 66 | print.PrintTargetBlocks(targets, false) 67 | } 68 | -------------------------------------------------------------------------------- /cmd/describe_tasks.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | "github.com/alajmo/sake/core/print" 9 | ) 10 | 11 | func describeTasksCmd(config *dao.Config, configErr *error) *cobra.Command { 12 | var taskFlags core.TaskFlags 13 | 14 | cmd := cobra.Command{ 15 | Aliases: []string{"task", "tsk"}, 16 | Use: "tasks [tasks]", 17 | Short: "Describe tasks", 18 | Long: "Describe tasks.", 19 | Example: ` # Describe all tasks 20 | sake describe tasks 21 | 22 | # Describe task 23 | sake describe task `, 24 | Run: func(cmd *cobra.Command, args []string) { 25 | core.CheckIfError(*configErr) 26 | describe(config, args, taskFlags) 27 | }, 28 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 29 | if *configErr != nil { 30 | return []string{}, cobra.ShellCompDirectiveDefault 31 | } 32 | 33 | values := config.GetTaskIDAndDesc() 34 | return values, cobra.ShellCompDirectiveNoFileComp 35 | }, 36 | DisableAutoGenTag: true, 37 | } 38 | 39 | cmd.Flags().SortFlags = false 40 | 41 | cmd.Flags().BoolVarP(&taskFlags.Edit, "edit", "e", false, "edit task") 42 | 43 | return &cmd 44 | } 45 | 46 | func describe(config *dao.Config, args []string, taskFlags core.TaskFlags) { 47 | if taskFlags.Edit { 48 | if len(args) > 0 { 49 | err := config.EditTask(args[0]) 50 | core.CheckIfError(err) 51 | } else { 52 | err := config.EditTask("") 53 | core.CheckIfError(err) 54 | } 55 | } else { 56 | tasks, err := config.GetTasksByIDs(args) 57 | core.CheckIfError(err) 58 | 59 | if len(tasks) > 0 { 60 | for i := range tasks { 61 | for j := range tasks[i].Tasks { 62 | envs, err := dao.ParseTaskEnv(tasks[i].Tasks[j].Envs, []string{}, []string{}, []string{}) 63 | core.CheckIfError(err) 64 | 65 | tasks[i].Tasks[j].Envs = envs 66 | } 67 | } 68 | 69 | print.PrintTaskBlock(tasks) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /cmd/edit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | ) 9 | 10 | func editCmd(config *dao.Config, configErr *error) *cobra.Command { 11 | cmd := cobra.Command{ 12 | Aliases: []string{"e", "ed"}, 13 | Use: "edit [flags]", 14 | Short: "Open up sake config file in $EDITOR", 15 | Long: "Open up sake config file in $EDITOR.", 16 | Example: ` # Edit current context 17 | sake edit`, 18 | Run: func(cmd *cobra.Command, args []string) { 19 | err := *configErr 20 | switch e := err.(type) { 21 | case *core.ConfigNotFound: 22 | core.CheckIfError(e) 23 | default: 24 | runEdit(*config) 25 | } 26 | }, 27 | DisableAutoGenTag: true, 28 | } 29 | 30 | cmd.AddCommand( 31 | editServer(config, configErr), 32 | editTask(config, configErr), 33 | editTarget(config, configErr), 34 | editSpec(config, configErr), 35 | ) 36 | 37 | return &cmd 38 | } 39 | 40 | func runEdit(config dao.Config) { 41 | err := config.EditConfig() 42 | core.CheckIfError(err) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/edit_servers.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | ) 9 | 10 | func editServer(config *dao.Config, configErr *error) *cobra.Command { 11 | cmd := cobra.Command{ 12 | Aliases: []string{"servers", "serv"}, 13 | Use: "server [server]", 14 | Short: "Edit server", 15 | Long: `Open up sake config file in $EDITOR and go to servers section.`, 16 | Example: ` # Edit servers 17 | sake edit server 18 | 19 | # Edit server 20 | sake edit server `, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | err := *configErr 23 | switch e := err.(type) { 24 | case *core.ConfigNotFound: 25 | core.CheckIfError(e) 26 | default: 27 | runEditServer(args, *config) 28 | } 29 | }, 30 | Args: cobra.MaximumNArgs(1), 31 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 32 | if *configErr != nil || len(args) == 1 { 33 | return []string{}, cobra.ShellCompDirectiveDefault 34 | } 35 | values := config.GetServerGroupsAndDesc() 36 | return values, cobra.ShellCompDirectiveNoFileComp 37 | }, 38 | DisableAutoGenTag: true, 39 | } 40 | 41 | return &cmd 42 | } 43 | 44 | func runEditServer(args []string, config dao.Config) { 45 | if len(args) > 0 { 46 | err := config.EditServer(args[0]) 47 | core.CheckIfError(err) 48 | } else { 49 | err := config.EditServer("") 50 | core.CheckIfError(err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cmd/edit_spec.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | ) 9 | 10 | func editSpec(config *dao.Config, configErr *error) *cobra.Command { 11 | cmd := cobra.Command{ 12 | Aliases: []string{"specs", "sp"}, 13 | Use: "spec [spec]", 14 | Short: "Edit spec", 15 | Long: `Open up sake config file in $EDITOR and go to specs section.`, 16 | Example: ` # Edit specs 17 | sake edit spec 18 | 19 | # Edit spec 20 | sake edit spec `, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | err := *configErr 23 | switch e := err.(type) { 24 | case *core.ConfigNotFound: 25 | core.CheckIfError(e) 26 | default: 27 | runEditSpec(args, *config) 28 | } 29 | }, 30 | Args: cobra.MaximumNArgs(1), 31 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 32 | if *configErr != nil || len(args) == 1 { 33 | return []string{}, cobra.ShellCompDirectiveDefault 34 | } 35 | 36 | return config.GetSpecNames(), cobra.ShellCompDirectiveNoFileComp 37 | }, 38 | DisableAutoGenTag: true, 39 | } 40 | 41 | return &cmd 42 | } 43 | 44 | func runEditSpec(args []string, config dao.Config) { 45 | if len(args) > 0 { 46 | err := config.EditSpec(args[0]) 47 | core.CheckIfError(err) 48 | } else { 49 | err := config.EditSpec("") 50 | core.CheckIfError(err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cmd/edit_target.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | ) 9 | 10 | func editTarget(config *dao.Config, configErr *error) *cobra.Command { 11 | cmd := cobra.Command{ 12 | Aliases: []string{"targets", "targ"}, 13 | Use: "target [target]", 14 | Short: "Edit target", 15 | Long: `Open up sake config file in $EDITOR and go to targets section.`, 16 | Example: ` # Edit targets 17 | sake edit target 18 | 19 | # Edit target 20 | sake edit target `, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | err := *configErr 23 | switch e := err.(type) { 24 | case *core.ConfigNotFound: 25 | core.CheckIfError(e) 26 | default: 27 | runEditTarget(args, *config) 28 | } 29 | }, 30 | Args: cobra.MaximumNArgs(1), 31 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 32 | if *configErr != nil || len(args) == 1 { 33 | return []string{}, cobra.ShellCompDirectiveDefault 34 | } 35 | 36 | return config.GetTargetNames(), cobra.ShellCompDirectiveNoFileComp 37 | }, 38 | DisableAutoGenTag: true, 39 | } 40 | 41 | return &cmd 42 | } 43 | 44 | func runEditTarget(args []string, config dao.Config) { 45 | if len(args) > 0 { 46 | err := config.EditTarget(args[0]) 47 | core.CheckIfError(err) 48 | } else { 49 | err := config.EditTarget("") 50 | core.CheckIfError(err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cmd/edit_task.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | ) 9 | 10 | func editTask(config *dao.Config, configErr *error) *cobra.Command { 11 | cmd := cobra.Command{ 12 | Aliases: []string{"tasks", "tsk"}, 13 | Use: "task [task]", 14 | Short: "Edit task", 15 | Long: `Open up sake config file in $EDITOR and go to tasks section.`, 16 | Example: ` # Edit tasks 17 | sake edit task 18 | 19 | # Edit task 20 | sake edit task `, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | err := *configErr 23 | switch e := err.(type) { 24 | case *core.ConfigNotFound: 25 | core.CheckIfError(e) 26 | default: 27 | runEditTask(args, *config) 28 | } 29 | }, 30 | Args: cobra.MaximumNArgs(1), 31 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 32 | if *configErr != nil || len(args) == 1 { 33 | return []string{}, cobra.ShellCompDirectiveDefault 34 | } 35 | 36 | return config.GetTaskIDAndDesc(), cobra.ShellCompDirectiveNoFileComp 37 | }, 38 | DisableAutoGenTag: true, 39 | } 40 | 41 | return &cmd 42 | } 43 | 44 | func runEditTask(args []string, config dao.Config) { 45 | if len(args) > 0 { 46 | err := config.EditTask(args[0]) 47 | core.CheckIfError(err) 48 | } else { 49 | err := config.EditTask("") 50 | core.CheckIfError(err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cmd/gen.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | ) 8 | 9 | func genCmd() *cobra.Command { 10 | dir := "" 11 | cmd := cobra.Command{ 12 | Use: "gen", 13 | Short: "Generate man page", 14 | Long: "Generate man page", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | err := core.GenManPages(dir) 17 | core.CheckIfError(err) 18 | }, 19 | 20 | DisableAutoGenTag: true, 21 | } 22 | cmd.Flags().SortFlags = false 23 | 24 | cmd.Flags().StringVarP(&dir, "dir", "d", "./", "directory to save manpage to") 25 | err := cmd.RegisterFlagCompletionFunc("dir", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 26 | return nil, cobra.ShellCompDirectiveFilterDirs 27 | }) 28 | core.CheckIfError(err) 29 | 30 | return &cmd 31 | } 32 | -------------------------------------------------------------------------------- /cmd/gen_docs.go: -------------------------------------------------------------------------------- 1 | // This source will generate 2 | // - core/sake.1 3 | // - docs/command-reference.md 4 | // 5 | // and is not included in the final build. 6 | 7 | package cmd 8 | 9 | import ( 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/alajmo/sake/core" 13 | ) 14 | 15 | func genDocsCmd(longAppDesc string) *cobra.Command { 16 | cmd := cobra.Command{ 17 | Use: "gen-docs", 18 | Short: "Generate man and markdown pages", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | err := core.CreateManPage( 21 | longAppDesc, 22 | version, 23 | date, 24 | rootCmd, 25 | checkCmd(&configErr), 26 | runCmd(&config, &configErr), 27 | execCmd(&config, &configErr), 28 | initCmd(), 29 | editCmd(&config, &configErr), 30 | listCmd(&config, &configErr), 31 | describeCmd(&config, &configErr), 32 | sshCmd(&config, &configErr), 33 | genCmd(), 34 | ) 35 | core.CheckIfError(err) 36 | }, 37 | 38 | DisableAutoGenTag: true, 39 | } 40 | 41 | return &cmd 42 | } 43 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/alajmo/sake/core" 9 | "github.com/alajmo/sake/core/dao" 10 | "github.com/alajmo/sake/core/print" 11 | ) 12 | 13 | func initCmd() *cobra.Command { 14 | cmd := cobra.Command{ 15 | Use: "init [flags]", 16 | Short: "Initialize sake in the current directory", 17 | Long: "Initialize sake in the current directory.", 18 | Example: ` # Basic example 19 | sake init`, 20 | 21 | Args: cobra.MaximumNArgs(1), 22 | Run: func(cmd *cobra.Command, args []string) { 23 | servers, err := dao.InitSake(args) 24 | core.CheckIfError(err) 25 | PrintServerInit(servers) 26 | }, 27 | DisableAutoGenTag: true, 28 | } 29 | 30 | return &cmd 31 | } 32 | 33 | func PrintServerInit(servers []dao.Server) { 34 | theme := dao.Theme{ 35 | Table: dao.DefaultTable, 36 | } 37 | 38 | options := print.PrintTableOptions{ 39 | Theme: theme, 40 | OmitEmptyRows: true, 41 | OmitEmptyColumns: false, 42 | Output: "table", 43 | } 44 | 45 | data := dao.TableOutput{ 46 | Headers: []string{"server", "host"}, 47 | Rows: []dao.Row{}, 48 | } 49 | 50 | for _, server := range servers { 51 | data.Rows = append(data.Rows, dao.Row{Columns: []string{server.Name, server.Host}}) 52 | } 53 | 54 | fmt.Println("\nFollowing servers were added to sake.yaml") 55 | err := print.PrintTable(data.Rows, options, data.Headers, []string{}, true, true) 56 | core.CheckIfError(err) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | ) 9 | 10 | func listCmd(config *dao.Config, configErr *error) *cobra.Command { 11 | var listFlags core.ListFlags 12 | 13 | cmd := cobra.Command{ 14 | Aliases: []string{"ls", "l"}, 15 | Use: "list", 16 | Short: "List servers, tasks, tags, specs and targets", 17 | Long: "List servers, tasks, tags, specs and targets", 18 | Example: ` # List all servers 19 | sake list servers 20 | 21 | # List all tasks 22 | sake list tasks 23 | 24 | # List all tags 25 | sake list tags`, 26 | DisableAutoGenTag: true, 27 | } 28 | cmd.PersistentFlags().SortFlags = false 29 | cmd.Flags().SortFlags = false 30 | 31 | cmd.AddCommand( 32 | listServersCmd(config, configErr, &listFlags), 33 | listTasksCmd(config, configErr, &listFlags), 34 | listTagsCmd(config, configErr, &listFlags), 35 | listTargetsCmd(config, configErr, &listFlags), 36 | listSpecsCmd(config, configErr, &listFlags), 37 | ) 38 | 39 | cmd.PersistentFlags().StringVarP(&listFlags.Output, "output", "o", "table", "set table output [table|table-2|table-3|table-4|html|markdown|json|csv]") 40 | err := cmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 41 | if *configErr != nil { 42 | return []string{}, cobra.ShellCompDirectiveDefault 43 | } 44 | valid := []string{"table", "table-2", "table-3", "table-4", "html", "markdown", "json", "csv"} 45 | return valid, cobra.ShellCompDirectiveDefault 46 | }) 47 | core.CheckIfError(err) 48 | 49 | cmd.PersistentFlags().StringVar(&listFlags.Theme, "theme", "default", "set theme") 50 | err = cmd.RegisterFlagCompletionFunc("theme", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 51 | if *configErr != nil { 52 | return []string{}, cobra.ShellCompDirectiveDefault 53 | } 54 | names := config.GetThemeNames() 55 | return names, cobra.ShellCompDirectiveDefault 56 | }) 57 | core.CheckIfError(err) 58 | 59 | return &cmd 60 | } 61 | -------------------------------------------------------------------------------- /cmd/list_specs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | "github.com/alajmo/sake/core/print" 9 | ) 10 | 11 | var specHeaders = []string{"spec", "desc", "describe", "list_hosts", "order", "silent", "hidden", "strategy", "batch", "batch_p", "forks", "output", "print", "any_errors_fatal", "max_fail_percentage", "ignore_errors", "ignore_unreachable", "omit_empty", "report", "verbose", "confirm", "step"} 12 | 13 | func listSpecsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command { 14 | var specFlags core.SpecFlags 15 | 16 | cmd := cobra.Command{ 17 | Aliases: []string{"spec"}, 18 | Use: "specs [specs]", 19 | Short: "List specs", 20 | Long: "List specs.", 21 | Example: ` # List all specs 22 | sake list specs`, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | core.CheckIfError(*configErr) 25 | listSpecs(config, args, listFlags, &specFlags) 26 | }, 27 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 28 | if *configErr != nil { 29 | return []string{}, cobra.ShellCompDirectiveDefault 30 | } 31 | 32 | specs := config.GetSpecNames() 33 | return specs, cobra.ShellCompDirectiveNoFileComp 34 | }, 35 | DisableAutoGenTag: true, 36 | } 37 | 38 | cmd.Flags().SortFlags = false 39 | 40 | cmd.Flags().StringSliceVar(&specFlags.Headers, "headers", specHeaders, "set headers") 41 | err := cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 42 | if *configErr != nil { 43 | return []string{}, cobra.ShellCompDirectiveDefault 44 | } 45 | 46 | validHeaders := specHeaders 47 | return validHeaders, cobra.ShellCompDirectiveDefault 48 | }) 49 | core.CheckIfError(err) 50 | 51 | return &cmd 52 | } 53 | 54 | func listSpecs( 55 | config *dao.Config, 56 | args []string, 57 | listFlags *core.ListFlags, 58 | specFlags *core.SpecFlags, 59 | ) { 60 | theme, err := config.GetTheme(listFlags.Theme) 61 | core.CheckIfError(err) 62 | 63 | options := print.PrintTableOptions{ 64 | Output: listFlags.Output, 65 | Theme: *theme, 66 | OmitEmptyRows: false, 67 | OmitEmptyColumns: true, 68 | Resource: "spec", 69 | } 70 | 71 | var specs []dao.Spec 72 | if len(args) > 0 { 73 | s, err := config.GetSpecsByName(args) 74 | core.CheckIfError(err) 75 | specs = s 76 | } else { 77 | specs = config.Specs 78 | } 79 | 80 | if len(specs) > 0 { 81 | rows := dao.GetTableData(specs, specFlags.Headers) 82 | err := print.PrintTable(rows, options, specFlags.Headers, []string{}, true, true) 83 | core.CheckIfError(err) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /cmd/list_tags.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | "github.com/alajmo/sake/core/print" 9 | ) 10 | 11 | var tagHeaders = []string{"tag", "server"} 12 | 13 | func listTagsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command { 14 | var tagFlags core.TagFlags 15 | 16 | cmd := cobra.Command{ 17 | Aliases: []string{"tag"}, 18 | Use: "tags [tags]", 19 | Short: "List tags", 20 | Long: "List tags.", 21 | Example: ` # List all tags 22 | sake list tags`, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | core.CheckIfError(*configErr) 25 | listTags(config, args, listFlags, &tagFlags) 26 | }, 27 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 28 | if *configErr != nil { 29 | return []string{}, cobra.ShellCompDirectiveDefault 30 | } 31 | 32 | tags := config.GetTags() 33 | return tags, cobra.ShellCompDirectiveNoFileComp 34 | }, 35 | DisableAutoGenTag: true, 36 | } 37 | 38 | cmd.Flags().SortFlags = false 39 | 40 | cmd.Flags().StringSliceVar(&tagFlags.Headers, "headers", tagHeaders, "set headers") 41 | err := cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 42 | if *configErr != nil { 43 | return []string{}, cobra.ShellCompDirectiveDefault 44 | } 45 | 46 | validHeaders := tagHeaders 47 | return validHeaders, cobra.ShellCompDirectiveDefault 48 | }) 49 | core.CheckIfError(err) 50 | 51 | return &cmd 52 | } 53 | 54 | func listTags( 55 | config *dao.Config, 56 | args []string, 57 | listFlags *core.ListFlags, 58 | tagFlags *core.TagFlags, 59 | ) { 60 | theme, err := config.GetTheme(listFlags.Theme) 61 | core.CheckIfError(err) 62 | 63 | options := print.PrintTableOptions{ 64 | Output: listFlags.Output, 65 | Theme: *theme, 66 | OmitEmptyRows: false, 67 | OmitEmptyColumns: true, 68 | Resource: "tag", 69 | } 70 | 71 | allTags := config.GetTags() 72 | 73 | if len(args) > 0 { 74 | foundTags := core.Intersection(args, allTags) 75 | // Could not find one of the provided tags 76 | if len(foundTags) != len(args) { 77 | core.CheckIfError(&core.TagNotFound{Tags: args}) 78 | } 79 | 80 | tags, err := config.GetTagAssocations(foundTags) 81 | core.CheckIfError(err) 82 | 83 | if len(tags) > 0 { 84 | err := print.PrintTable(tags, options, tagFlags.Headers, []string{}, true, true) 85 | core.CheckIfError(err) 86 | } 87 | } else { 88 | tags, err := config.GetTagAssocations(allTags) 89 | core.CheckIfError(err) 90 | if len(tags) > 0 { 91 | rows := dao.GetTableData(tags, tagFlags.Headers) 92 | err := print.PrintTable(rows, options, tagFlags.Headers, []string{}, true, true) 93 | core.CheckIfError(err) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /cmd/list_targets.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | "github.com/alajmo/sake/core/print" 9 | ) 10 | 11 | var targetHeaders = []string{"target", "desc", "all", "servers", "tags", "regex", "invert", "limit", "limit_p"} 12 | 13 | func listTargetsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command { 14 | var targetFlags core.TargetFlags 15 | 16 | cmd := cobra.Command{ 17 | Aliases: []string{"target"}, 18 | Use: "targets [targets]", 19 | Short: "List targets", 20 | Long: "List targets.", 21 | Example: ` # List all targets 22 | sake list targets`, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | core.CheckIfError(*configErr) 25 | listTargets(config, args, listFlags, &targetFlags) 26 | }, 27 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 28 | if *configErr != nil { 29 | return []string{}, cobra.ShellCompDirectiveDefault 30 | } 31 | 32 | targets := config.GetTargetNames() 33 | return targets, cobra.ShellCompDirectiveNoFileComp 34 | }, 35 | DisableAutoGenTag: true, 36 | } 37 | cmd.Flags().SortFlags = false 38 | 39 | cmd.Flags().StringSliceVar(&targetFlags.Headers, "headers", targetHeaders, "set headers. Available headers: name, regex") 40 | err := cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 41 | if *configErr != nil { 42 | return []string{}, cobra.ShellCompDirectiveDefault 43 | } 44 | 45 | validHeaders := targetHeaders 46 | return validHeaders, cobra.ShellCompDirectiveDefault 47 | }) 48 | core.CheckIfError(err) 49 | 50 | return &cmd 51 | } 52 | 53 | func listTargets( 54 | config *dao.Config, 55 | args []string, 56 | listFlags *core.ListFlags, 57 | targetFlags *core.TargetFlags, 58 | ) { 59 | theme, err := config.GetTheme(listFlags.Theme) 60 | core.CheckIfError(err) 61 | 62 | options := print.PrintTableOptions{ 63 | Output: listFlags.Output, 64 | Theme: *theme, 65 | OmitEmptyRows: false, 66 | OmitEmptyColumns: true, 67 | Resource: "target", 68 | } 69 | 70 | var targets []dao.Target 71 | if len(args) > 0 { 72 | t, err := config.GetTargetsByName(args) 73 | core.CheckIfError(err) 74 | targets = t 75 | } else { 76 | targets = config.Targets 77 | } 78 | 79 | if len(targets) > 0 { 80 | rows := dao.GetTableData(targets, targetFlags.Headers) 81 | err := print.PrintTable(rows, options, targetFlags.Headers, []string{}, true, true) 82 | core.CheckIfError(err) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /cmd/list_tasks.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | "github.com/alajmo/sake/core/print" 9 | ) 10 | 11 | var taskHeaders = []string{"task", "desc", "local", "tty", "attach", "work_dir", "shell", "spec", "target", "theme"} 12 | 13 | func listTasksCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command { 14 | var taskFlags core.TaskFlags 15 | 16 | cmd := cobra.Command{ 17 | Aliases: []string{"task", "tsk", "tsks"}, 18 | Use: "tasks [tasks]", 19 | Short: "List tasks", 20 | Long: "List tasks.", 21 | Example: ` # List all tasks 22 | sake list tasks 23 | 24 | # List task 25 | sake list task `, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | core.CheckIfError(*configErr) 28 | listTasks(config, args, listFlags, &taskFlags) 29 | }, 30 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 31 | if *configErr != nil { 32 | return []string{}, cobra.ShellCompDirectiveDefault 33 | } 34 | 35 | return config.GetTaskIDAndDesc(), cobra.ShellCompDirectiveNoFileComp 36 | }, 37 | DisableAutoGenTag: true, 38 | } 39 | 40 | cmd.Flags().SortFlags = false 41 | 42 | cmd.Flags().BoolVarP(&taskFlags.AllHeaders, "all-headers", "H", false, "select all task headers") 43 | cmd.Flags().StringSliceVar(&taskFlags.Headers, "headers", []string{"task", "desc"}, "set headers") 44 | err := cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 45 | if *configErr != nil { 46 | return []string{}, cobra.ShellCompDirectiveDefault 47 | } 48 | 49 | validHeaders := taskHeaders 50 | return validHeaders, cobra.ShellCompDirectiveDefault 51 | }) 52 | core.CheckIfError(err) 53 | cmd.MarkFlagsMutuallyExclusive("all-headers", "headers") 54 | 55 | return &cmd 56 | } 57 | 58 | func listTasks( 59 | config *dao.Config, 60 | args []string, 61 | listFlags *core.ListFlags, 62 | taskFlags *core.TaskFlags, 63 | ) { 64 | tasks, err := config.GetTasksByIDs(args) 65 | core.CheckIfError(err) 66 | 67 | theme, err := config.GetTheme(listFlags.Theme) 68 | core.CheckIfError(err) 69 | 70 | if len(tasks) > 0 { 71 | options := print.PrintTableOptions{ 72 | Output: listFlags.Output, 73 | Theme: *theme, 74 | OmitEmptyRows: false, 75 | OmitEmptyColumns: true, 76 | Resource: "task", 77 | } 78 | 79 | var headers []string 80 | if taskFlags.AllHeaders { 81 | headers = taskHeaders 82 | } else { 83 | headers = taskFlags.Headers 84 | } 85 | 86 | rows := dao.GetTableData(tasks, headers) 87 | err := print.PrintTable(rows, options, headers, []string{}, true, true) 88 | core.CheckIfError(err) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/alajmo/sake/core/dao" 11 | ) 12 | 13 | const ( 14 | appName = "sake" 15 | shortAppDesc = "sake is a task runner for local and remote hosts" 16 | longAppDesc = `sake is a task runner for local and remote hosts. 17 | 18 | You define servers and tasks in a sake.yaml config file and then run the tasks on the servers. 19 | ` 20 | ) 21 | 22 | var ( 23 | config dao.Config 24 | configErr error 25 | configPath string 26 | userConfigPath string 27 | sshConfigPath string 28 | noColor bool 29 | buildMode = "" 30 | version = "dev" 31 | commit = "none" 32 | date = "n/a" 33 | rootCmd = &cobra.Command{ 34 | Use: appName, 35 | Short: shortAppDesc, 36 | Long: longAppDesc, 37 | Version: version, 38 | } 39 | ) 40 | 41 | func Execute() { 42 | if err := rootCmd.Execute(); err != nil { 43 | // When user input's wrong command or flag 44 | os.Exit(1) 45 | } 46 | } 47 | 48 | func init() { 49 | if runtime.GOOS == "windows" { 50 | dao.DEFAULT_SHELL = "pwsh -NoProfile -command" 51 | } 52 | 53 | if runtime.GOOS == "darwin" { 54 | dao.DEFAULT_SHELL = "zsh -c" 55 | } 56 | 57 | cobra.OnInitialize(initConfig) 58 | 59 | cobra.EnableCommandSorting = false 60 | 61 | rootCmd.Flags().SortFlags = false 62 | rootCmd.PersistentFlags().SortFlags = false 63 | 64 | rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "specify config") 65 | rootCmd.PersistentFlags().StringVarP(&userConfigPath, "user-config", "u", "", "specify user config") 66 | rootCmd.PersistentFlags().StringVar(&sshConfigPath, "ssh-config", "", "specify ssh config") 67 | rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "disable color") 68 | 69 | rootCmd.AddCommand( 70 | initCmd(), 71 | listCmd(&config, &configErr), 72 | describeCmd(&config, &configErr), 73 | runCmd(&config, &configErr), 74 | execCmd(&config, &configErr), 75 | sshCmd(&config, &configErr), 76 | editCmd(&config, &configErr), 77 | checkCmd(&configErr), 78 | completionCmd(), 79 | genCmd(), 80 | ) 81 | 82 | rootCmd.SetVersionTemplate(fmt.Sprintf("Version: %-10s\nCommit: %-10s\nDate: %-10s\n", version, commit, date)) 83 | 84 | if buildMode == "man" { 85 | rootCmd.AddCommand(genDocsCmd(longAppDesc)) 86 | } 87 | } 88 | 89 | func initConfig() { 90 | config, configErr = dao.ReadConfig(configPath, userConfigPath, sshConfigPath, noColor) 91 | } 92 | -------------------------------------------------------------------------------- /cmd/ssh.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/alajmo/sake/core" 7 | "github.com/alajmo/sake/core/dao" 8 | "github.com/alajmo/sake/core/run" 9 | ) 10 | 11 | func sshCmd(config *dao.Config, configErr *error) *cobra.Command { 12 | var runFlags core.RunFlags 13 | 14 | cmd := cobra.Command{ 15 | Use: "ssh [flags]", 16 | Short: "ssh to server", 17 | Long: `ssh to server.`, 18 | 19 | Example: ` # ssh to server 20 | sake ssh `, 21 | Args: cobra.ExactArgs(1), 22 | Run: func(cmd *cobra.Command, args []string) { 23 | core.CheckIfError(*configErr) 24 | ssh(args, config, &runFlags) 25 | }, 26 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 27 | if *configErr != nil { 28 | return []string{}, cobra.ShellCompDirectiveDefault 29 | } 30 | 31 | return config.GetRemoteServerNameAndDesc(), cobra.ShellCompDirectiveNoFileComp 32 | }, 33 | DisableAutoGenTag: true, 34 | } 35 | 36 | cmd.Flags().SortFlags = false 37 | 38 | cmd.Flags().StringVarP(&runFlags.IdentityFile, "identity-file", "i", "", "set identity file for all servers") 39 | cmd.Flags().StringVar(&runFlags.Password, "password", "", "set ssh password for all servers") 40 | 41 | return &cmd 42 | } 43 | 44 | func ssh(args []string, config *dao.Config, runFlags *core.RunFlags) { 45 | server, err := config.GetServer(args[0]) 46 | core.CheckIfError(err) 47 | servers := []dao.Server{*server} 48 | 49 | errConnect, err := run.ParseServers(config.SSHConfigFile, &servers, runFlags, "inventory") 50 | if len(errConnect) > 0 { 51 | core.Exit(&errConnect[0]) 52 | } 53 | core.CheckIfError(err) 54 | 55 | err = run.SSHToServer(servers[0], config.DisableVerifyHost, config.KnownHostsFile) 56 | core.CheckIfError(err) 57 | } 58 | -------------------------------------------------------------------------------- /core/dao/common_test.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "testing" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | func TestParseEnvsYAML(t *testing.T) { 10 | var data = ` 11 | env: 12 | foo: "bar" 13 | hello: hello world 14 | script: $(echo 123) 15 | script: $echo 123) 16 | ` 17 | configYAML := ConfigYAML{} 18 | err := yaml.Unmarshal([]byte(data), &configYAML) 19 | if err != nil { 20 | t.Fatalf("%q", err) 21 | } 22 | 23 | envs := ParseNodeEnv(configYAML.Env) 24 | 25 | wanted := []string{ 26 | "foo=bar", 27 | "hello=hello world", 28 | "script=$(echo 123)", 29 | "script=$echo 123)", 30 | } 31 | 32 | for i := range wanted { 33 | if envs[i] != wanted[i] { 34 | t.Fatalf(`Wanted: %q, Found: %q`, wanted[i], envs[i]) 35 | } 36 | } 37 | } 38 | 39 | func TestEvaluateEnv(t *testing.T) { 40 | envs := []string{ 41 | "foo=bar", 42 | "hello=hello world", 43 | "script=$(echo 123)", 44 | "script=$echo 123)", 45 | } 46 | 47 | found, err := EvaluateEnv(envs) 48 | if err != nil { 49 | t.Fatalf("%q", err) 50 | } 51 | 52 | wanted := []string{ 53 | "foo=bar", 54 | "hello=hello world", 55 | "script=123\n", 56 | "script=$echo 123)", 57 | } 58 | 59 | for i := range wanted { 60 | if found[i] != wanted[i] { 61 | t.Fatalf(`Wanted: %q, Found: %q`, wanted[i], found[i]) 62 | } 63 | } 64 | } 65 | 66 | func TestMergeEnvs(t *testing.T) { 67 | envsA := []string{ 68 | "foo=bar", 69 | "hello=world", 70 | } 71 | 72 | envsB := []string{ 73 | "foo=hej", 74 | "script=$(echo 123)", 75 | "xyz=zyx", 76 | } 77 | 78 | merged := MergeEnvs(envsA, envsB) 79 | 80 | wanted := []string{ 81 | "foo=bar", 82 | "hello=world", 83 | "script=$(echo 123)", 84 | "xyz=zyx", 85 | } 86 | 87 | for i := range wanted { 88 | if merged[i] != wanted[i] { 89 | t.Fatalf(`Wanted: %q, Found: %q`, wanted[i], merged[i]) 90 | } 91 | } 92 | } 93 | 94 | func TestSelectFirstNonEmpty(t *testing.T) { 95 | values := []string{ 96 | "", 97 | "", 98 | "foo", 99 | "hello", 100 | "", 101 | } 102 | 103 | firstNonEmpty := SelectFirstNonEmpty(values...) 104 | 105 | if firstNonEmpty != "foo" { 106 | t.Fatalf(`Wanted: %q, Found: %q`, "foo", firstNonEmpty) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /core/dao/tag.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/alajmo/sake/core" 7 | ) 8 | 9 | type Tag struct { 10 | Name string 11 | Servers []string 12 | } 13 | 14 | func (t Tag) GetValue(key string, _ int) string { 15 | lkey := strings.ToLower(key) 16 | switch lkey { 17 | case "server": 18 | return strings.Join(t.Servers, "\n") 19 | case "name", "tag": 20 | return t.Name 21 | default: 22 | return "" 23 | } 24 | } 25 | 26 | func (c *Config) GetTags() []string { 27 | tags := []string{} 28 | for _, server := range c.Servers { 29 | for _, tag := range server.Tags { 30 | if !core.StringInSlice(tag, tags) { 31 | tags = append(tags, tag) 32 | } 33 | } 34 | } 35 | 36 | return tags 37 | } 38 | 39 | func (c *Config) GetTagAssocations(tags []string) ([]Tag, error) { 40 | t := []Tag{} 41 | 42 | for _, tag := range tags { 43 | servers, err := c.GetServersByTags([]string{tag}) 44 | if err != nil { 45 | return []Tag{}, err 46 | } 47 | 48 | var serverNames []string 49 | for _, p := range servers { 50 | serverNames = append(serverNames, p.Name) 51 | } 52 | 53 | t = append(t, Tag{Name: tag, Servers: serverNames}) 54 | } 55 | 56 | return t, nil 57 | } 58 | -------------------------------------------------------------------------------- /core/flags.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // CMD Flags 4 | 5 | type ListFlags struct { 6 | Output string 7 | Theme string 8 | } 9 | 10 | type ServerFlags struct { 11 | Tags []string 12 | Headers []string 13 | Edit bool 14 | Regex string 15 | Invert bool 16 | AllHeaders bool 17 | } 18 | 19 | type TargetFlags struct { 20 | Headers []string 21 | Edit bool 22 | } 23 | 24 | type SpecFlags struct { 25 | Headers []string 26 | Edit bool 27 | } 28 | 29 | type TagFlags struct { 30 | Headers []string 31 | } 32 | 33 | type TaskFlags struct { 34 | Headers []string 35 | Edit bool 36 | AllHeaders bool 37 | } 38 | 39 | type RunFlags struct { 40 | // Flags 41 | Edit bool 42 | DryRun bool 43 | Describe bool 44 | ListHosts bool 45 | Silent bool 46 | Confirm bool 47 | Step bool 48 | Verbose bool 49 | 50 | // Reports 51 | Report []string 52 | 53 | // Target 54 | All bool 55 | Regex string 56 | Servers []string 57 | Tags []string 58 | Cwd bool 59 | Invert bool 60 | Limit uint32 61 | LimitP uint8 62 | Target string 63 | Order string 64 | 65 | // Config 66 | KnownHostsFile string 67 | 68 | // Task 69 | Theme string 70 | TTY bool 71 | Attach bool 72 | Local bool 73 | 74 | // Server 75 | IdentityFile string 76 | User string 77 | Password string 78 | 79 | // Spec 80 | Spec string 81 | AnyErrorsFatal bool 82 | MaxFailPercentage uint8 83 | IgnoreErrors bool 84 | IgnoreUnreachable bool 85 | OmitEmptyRows bool 86 | OmitEmptyColumns bool 87 | Forks uint32 88 | Batch uint32 89 | BatchP uint8 90 | Output string 91 | Print string 92 | Strategy string 93 | } 94 | 95 | type SetRunFlags struct { 96 | Silent bool 97 | Describe bool 98 | ListHosts bool 99 | Attach bool 100 | All bool 101 | Invert bool 102 | OmitEmptyRows bool 103 | OmitEmptyColumns bool 104 | Local bool 105 | TTY bool 106 | AnyErrorsFatal bool 107 | IgnoreErrors bool 108 | IgnoreUnreachable bool 109 | Order bool 110 | Report bool 111 | Forks bool 112 | Batch bool 113 | BatchP bool 114 | Servers bool 115 | Tags bool 116 | Regex bool 117 | Limit bool 118 | LimitP bool 119 | Verbose bool 120 | Confirm bool 121 | Step bool 122 | MaxFailPercentage bool 123 | } 124 | -------------------------------------------------------------------------------- /core/man.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | //go:embed sake.1 11 | var CONFIG_MAN []byte 12 | 13 | func GenManPages(dir string) error { 14 | manPath := filepath.Join(dir, "sake.1") 15 | err := os.WriteFile(manPath, CONFIG_MAN, 0644) 16 | CheckIfError(err) 17 | 18 | fmt.Printf("Created %s\n", manPath) 19 | 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /core/prefixer.go: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/goware/prefixer 2 | package core 3 | 4 | import ( 5 | "bufio" 6 | "io" 7 | ) 8 | 9 | // Prefixer implements io.Reader and io.WriterTo. It reads 10 | // data from the underlying reader and prepends every line 11 | // with a given string. 12 | type Prefixer struct { 13 | reader *bufio.Reader 14 | prefix []byte 15 | unread []byte 16 | eof bool 17 | } 18 | 19 | // New creates a new instance of Prefixer. 20 | func NewPrefixer(r io.Reader, prefix string) *Prefixer { 21 | return &Prefixer{ 22 | reader: bufio.NewReader(r), 23 | prefix: []byte(prefix), 24 | } 25 | } 26 | 27 | // Read implements io.Reader. It reads data into p from the 28 | // underlying reader and prepends every line with a prefix. 29 | // It does not block if no data is available yet. 30 | // It returns the number of bytes read into p. 31 | func (r *Prefixer) Read(p []byte) (n int, err error) { 32 | for { 33 | // Write unread data from previous read. 34 | if len(r.unread) > 0 { 35 | m := copy(p[n:], r.unread) 36 | n += m 37 | r.unread = r.unread[m:] 38 | if len(r.unread) > 0 { 39 | return n, nil 40 | } 41 | } 42 | 43 | // The underlying Reader already returned EOF, do not read again. 44 | if r.eof { 45 | return n, io.EOF 46 | } 47 | 48 | // Read new line, including delim. 49 | r.unread, err = r.reader.ReadBytes('\n') 50 | 51 | if err == io.EOF { 52 | r.eof = true 53 | } 54 | 55 | // No new data, do not block. 56 | if len(r.unread) == 0 { 57 | return n, err 58 | } 59 | 60 | // Some new data, prepend prefix. 61 | // TODO: We could write the prefix to r.unread buffer just once 62 | // and re-use it instead of prepending every time. 63 | r.unread = append(r.prefix, r.unread...) 64 | 65 | if err != nil { 66 | if err == io.EOF && len(r.unread) > 0 { 67 | // The underlying Reader already returned EOF, but we still 68 | // have some unread data to send, thus clear the error. 69 | return n, nil 70 | } 71 | return n, err 72 | } 73 | } 74 | } 75 | 76 | func (r *Prefixer) WriteTo(w io.Writer) (n int64, err error) { 77 | for { 78 | // Write unread data from previous read. 79 | if len(r.unread) > 0 { 80 | m, err := w.Write(r.unread) 81 | n += int64(m) 82 | if err != nil { 83 | return n, err 84 | } 85 | r.unread = r.unread[m:] 86 | if len(r.unread) > 0 { 87 | return n, nil 88 | } 89 | } 90 | 91 | // The underlying Reader already returned EOF, do not read again. 92 | if r.eof { 93 | return n, io.EOF 94 | } 95 | 96 | // Read new line, including delim. 97 | r.unread, err = r.reader.ReadBytes('\n') 98 | 99 | if err == io.EOF { 100 | r.eof = true 101 | } 102 | 103 | // No new data, do not block. 104 | if len(r.unread) == 0 { 105 | return n, err 106 | } 107 | 108 | // TODO: rewrite the prefix to r.unread buffer 109 | // just once and re-use it instead of prepending every time 110 | r.unread = append(r.prefix, r.unread...) 111 | 112 | if err != nil { 113 | if err == io.EOF && len(r.unread) > 0 { 114 | // The underlying Reader already returned EOF, but we still 115 | // have some unread data to send, thus clear the error. 116 | return n, nil 117 | } 118 | return n, err 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /core/print/table.go: -------------------------------------------------------------------------------- 1 | package print 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/jedib0t/go-pretty/v6/table" 8 | 9 | "github.com/alajmo/sake/core/dao" 10 | ) 11 | 12 | func CreateTable( 13 | options PrintTableOptions, 14 | tableHeaders []string, 15 | ) table.Writer { 16 | t := table.NewWriter() 17 | 18 | theme := options.Theme 19 | 20 | t.SetOutputMirror(os.Stdout) 21 | 22 | t.SetStyle(FormatTable(theme)) 23 | if options.OmitEmptyColumns { 24 | t.SuppressEmptyColumns() 25 | } 26 | 27 | var headers []table.ColumnConfig 28 | for i := range tableHeaders { 29 | hh := table.ColumnConfig{ 30 | Number: i + 1, 31 | AlignHeader: GetAlign(*theme.Table.Header.Align), 32 | Align: GetAlign(*theme.Table.Row.Align), 33 | ColorsHeader: combineColors(theme.Table.Header.Fg, theme.Table.Header.Bg, theme.Table.Header.Attr), 34 | Colors: combineColors(theme.Table.Row.Fg, theme.Table.Row.Bg, theme.Table.Row.Attr), 35 | ColorsFooter: combineColors(theme.Table.Footer.Fg, theme.Table.Footer.Bg, theme.Table.Footer.Attr), 36 | } 37 | 38 | headers = append(headers, hh) 39 | } 40 | 41 | t.SetColumnConfigs(headers) 42 | 43 | return t 44 | } 45 | 46 | func FormatTable(theme dao.Theme) table.Style { 47 | return table.Style{ 48 | Name: theme.Name, 49 | Box: theme.Table.Box, 50 | 51 | Format: table.FormatOptions{ 52 | Header: GetFormat(*theme.Table.Header.Format), 53 | Row: GetFormat(*theme.Table.Row.Format), 54 | Footer: GetFormat(*theme.Table.Footer.Format), 55 | }, 56 | 57 | Options: table.Options{ 58 | DrawBorder: *theme.Table.Options.DrawBorder, 59 | SeparateColumns: *theme.Table.Options.SeparateColumns, 60 | SeparateHeader: *theme.Table.Options.SeparateHeader, 61 | SeparateRows: *theme.Table.Options.SeparateRows, 62 | SeparateFooter: *theme.Table.Options.SeparateFooter, 63 | }, 64 | 65 | Title: table.TitleOptions{ 66 | Align: GetAlign(*theme.Table.Title.Align), 67 | Colors: combineColors(theme.Table.Title.Fg, theme.Table.Title.Bg, theme.Table.Title.Attr), 68 | }, 69 | 70 | // Border colors 71 | Color: table.ColorOptions{ 72 | Header: combineColors(theme.Table.Border.Header.Fg, theme.Table.Border.Header.Bg, theme.Table.Border.Header.Attr), 73 | Row: combineColors(theme.Table.Border.Row.Fg, theme.Table.Border.Row.Bg, theme.Table.Border.Row.Attr), 74 | RowAlternate: combineColors(theme.Table.Border.RowAlternate.Fg, theme.Table.Border.RowAlternate.Bg, theme.Table.Border.RowAlternate.Attr), 75 | Footer: combineColors(theme.Table.Border.Footer.Fg, theme.Table.Border.Footer.Bg, theme.Table.Border.Footer.Attr), 76 | }, 77 | } 78 | } 79 | 80 | func RenderTable( 81 | t table.Writer, 82 | output string, 83 | padTop bool, 84 | padBottom bool, 85 | ) { 86 | switch output { 87 | case "markdown": 88 | t.RenderMarkdown() 89 | case "html": 90 | t.RenderHTML() 91 | case "csv": 92 | t.RenderCSV() 93 | default: 94 | if padTop { 95 | fmt.Println() 96 | } 97 | t.Render() 98 | if padBottom { 99 | fmt.Println() 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/run/client.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "sync" 7 | ) 8 | 9 | type Client interface { 10 | Connect(SSHDialFunc, bool, string, uint, *sync.Mutex) *ErrConnect 11 | Run(int, []string, string, string, string) error 12 | Wait(int) error 13 | Close(int) error 14 | Write(int, []byte) (n int, err error) 15 | WriteClose(int) error 16 | Stdin(int) io.WriteCloser 17 | Stderr(int) io.Reader 18 | Stdout(int) io.Reader 19 | Signal(int, os.Signal) error 20 | GetName() string 21 | Prefix() (string, string, string, uint16) 22 | Connected() bool 23 | } 24 | 25 | type ErrConnect struct { 26 | Name string 27 | Host string 28 | User string 29 | Port uint16 30 | Reason string 31 | } 32 | 33 | func (e *ErrConnect) Error() string { 34 | return e.Reason 35 | } 36 | -------------------------------------------------------------------------------- /core/run/exec_test.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alajmo/sake/core/test" 7 | ) 8 | 9 | func TestWorkDir(t *testing.T) { 10 | // Remote 11 | 12 | test.CheckEqS(t, getWorkDir(false, false, "", "", "cmd-root", "server-root"), "") 13 | test.CheckEqS(t, getWorkDir(false, false, "cmd", "", "cmd-root", "server-root"), "cmd") 14 | test.CheckEqS(t, getWorkDir(false, false, "", "server", "", "server-root"), "server") 15 | test.CheckEqS(t, getWorkDir(false, false, "cmd", "server", "cmd-root", "server-root"), "server/cmd") 16 | 17 | // Local 18 | 19 | test.CheckEqS(t, getWorkDir(true, false, "", "", "cmd-root", "server-root"), "cmd-root") 20 | test.CheckEqS(t, getWorkDir(true, true, "", "", "cmd-root", "server-root"), "cmd-root") 21 | test.CheckEqS(t, getWorkDir(true, false, "", "server", "cmd-root", "server-root"), "cmd-root") 22 | test.CheckEqS(t, getWorkDir(false, true, "", "", "cmd-root", "server-root"), "cmd-root") 23 | 24 | test.CheckEqS(t, getWorkDir(false, true, "cmd", "server", "cmd-root", "server-root"), "server-root/server/cmd") 25 | test.CheckEqS(t, getWorkDir(true, true, "cmd", "server", "", "server-root"), "server-root/server/cmd") 26 | 27 | test.CheckEqS(t, getWorkDir(true, false, "cmd", "", "cmd-root", "server-root"), "cmd-root/cmd") 28 | test.CheckEqS(t, getWorkDir(true, false, "cmd", "server", "cmd-root", "server-root"), "cmd-root/cmd") 29 | test.CheckEqS(t, getWorkDir(true, true, "cmd", "", "cmd-root", "server-root"), "cmd-root/cmd") 30 | test.CheckEqS(t, getWorkDir(false, true, "cmd", "", "cmd-root", "server-root"), "cmd-root/cmd") 31 | 32 | test.CheckEqS(t, getWorkDir(false, true, "", "server", "cmd-root", "server-root"), "server-root/server") 33 | test.CheckEqS(t, getWorkDir(true, true, "", "server", "cmd-root", "server-root"), "server-root/server") 34 | } 35 | -------------------------------------------------------------------------------- /core/run/localhost.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "fmt" 5 | "github.com/alajmo/sake/core/dao" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | // Client is a wrapper over the SSH connection/Sessions. 14 | type LocalhostClient struct { 15 | Name string 16 | User string 17 | Host string 18 | Port uint16 19 | 20 | Sessions []LocalSession 21 | } 22 | 23 | type LocalSession struct { 24 | stdin io.WriteCloser 25 | cmd *exec.Cmd 26 | stdout io.Reader 27 | stderr io.Reader 28 | running bool 29 | } 30 | 31 | func (c *LocalhostClient) Connect(dialer SSHDialFunc, _ bool, _ string, _ uint, mu *sync.Mutex) *ErrConnect { 32 | return nil 33 | } 34 | 35 | func (c *LocalhostClient) Run(i int, env []string, workDir string, shell string, cmdStr string) error { 36 | var err error 37 | 38 | if c.Sessions[i].running { 39 | return fmt.Errorf("command already running") 40 | } 41 | 42 | userEnv := os.Environ() 43 | 44 | if shell == "" { 45 | shell = dao.DEFAULT_SHELL 46 | } 47 | 48 | var cmdString string 49 | if workDir != "" { 50 | cmdString = fmt.Sprintf("cd %s; %s", workDir, cmdStr) 51 | } else { 52 | cmdString = cmdStr 53 | } 54 | 55 | args := strings.SplitN(shell, " ", 2) 56 | shellProgram := args[0] 57 | shellArgs := append(args[1:], cmdString) 58 | 59 | cmd := exec.Command(shellProgram, shellArgs...) 60 | cmd.Env = append(userEnv, env...) 61 | c.Sessions[i].cmd = cmd 62 | 63 | c.Sessions[i].stdout, err = cmd.StdoutPipe() 64 | if err != nil { 65 | return err 66 | } 67 | 68 | c.Sessions[i].stderr, err = cmd.StderrPipe() 69 | if err != nil { 70 | return err 71 | } 72 | 73 | c.Sessions[i].stdin, err = cmd.StdinPipe() 74 | if err != nil { 75 | return err 76 | } 77 | 78 | if err := c.Sessions[i].cmd.Start(); err != nil { 79 | return err 80 | } 81 | 82 | c.Sessions[i].running = true 83 | return nil 84 | } 85 | 86 | func (c *LocalhostClient) Wait(i int) error { 87 | if !c.Sessions[i].running { 88 | return fmt.Errorf("trying to wait on stopped command") 89 | } 90 | err := c.Sessions[i].cmd.Wait() 91 | c.Sessions[i].running = false 92 | return err 93 | } 94 | 95 | func (c *LocalhostClient) Close(i int) error { 96 | return nil 97 | } 98 | 99 | func (c *LocalhostClient) Stdin(i int) io.WriteCloser { 100 | return c.Sessions[i].stdin 101 | } 102 | 103 | func (c *LocalhostClient) Write(i int, p []byte) (n int, err error) { 104 | return c.Sessions[i].stdin.Write(p) 105 | } 106 | 107 | func (c *LocalhostClient) WriteClose(i int) error { 108 | return c.Sessions[i].stdin.Close() 109 | } 110 | 111 | func (c *LocalhostClient) Stderr(i int) io.Reader { 112 | return c.Sessions[i].stderr 113 | } 114 | 115 | func (c *LocalhostClient) Stdout(i int) io.Reader { 116 | return c.Sessions[i].stdout 117 | } 118 | 119 | func (c *LocalhostClient) Prefix() (string, string, string, uint16) { 120 | return c.Name, c.Host, c.User, c.Port 121 | } 122 | 123 | func (c *LocalhostClient) Signal(i int, sig os.Signal) error { 124 | return c.Sessions[i].cmd.Process.Signal(sig) 125 | } 126 | 127 | func (c *LocalhostClient) GetName() string { 128 | return c.Name 129 | } 130 | 131 | func (c *LocalhostClient) Connected() bool { 132 | return true 133 | } 134 | -------------------------------------------------------------------------------- /core/run/unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package run 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | 12 | "github.com/alajmo/sake/core/dao" 13 | "golang.org/x/sys/unix" 14 | ) 15 | 16 | // func SSHToServer(host string, user string, port uint16, bastion string, disableVerifyHost bool, knownHostFile string) error { 17 | func SSHToServer(server dao.Server, disableVerifyHost bool, knownHostFile string) error { 18 | sshBin, err := exec.LookPath("ssh") 19 | if err != nil { 20 | return err 21 | } 22 | 23 | sshConnStr := fmt.Sprintf("%s@%s", server.User, server.Host) 24 | portStr := fmt.Sprintf("-p %d", server.Port) 25 | 26 | args := []string{"ssh", "-t", sshConnStr, portStr} 27 | if disableVerifyHost { 28 | args = append(args, "-o StrictHostKeyChecking=no") 29 | } else { 30 | args = append(args, fmt.Sprintf("-o UserKnownHostsFile=%s", knownHostFile)) 31 | } 32 | 33 | // TODO: 34 | if len(server.Bastions) > 0 { 35 | jumphosts := []string{} 36 | for _, bastion := range server.Bastions { 37 | jumphosts = append(jumphosts, fmt.Sprintf("%s@%s:%d", bastion.User, bastion.Host, bastion.Port)) 38 | } 39 | 40 | args = append(args, fmt.Sprintf("-J %s", strings.Join(jumphosts, ","))) 41 | } 42 | 43 | err = unix.Exec(sshBin, args, os.Environ()) 44 | 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func ExecTTY(cmd string, envs []string) error { 53 | shell := "bash" 54 | foundShell, found := os.LookupEnv("SHELL") 55 | if found { 56 | shell = foundShell 57 | } 58 | 59 | execBin, err := exec.LookPath(shell) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | userEnv := append(os.Environ(), envs...) 65 | err = unix.Exec(execBin, []string{shell, "-c", cmd}, userEnv) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /core/run/windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package run 5 | 6 | import ( 7 | "github.com/alajmo/sake/core/dao" 8 | ) 9 | 10 | func SSHToServer(server dao.Server, disableVerifyHost bool, knownHostFile string) error { 11 | return nil 12 | } 13 | 14 | func ExecTTY(cmd string, envs []string) error { 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /core/spinner.go: -------------------------------------------------------------------------------- 1 | // Singleton module 2 | package core 3 | 4 | import ( 5 | "io" 6 | "os" 7 | "os/signal" 8 | "sync" 9 | "time" 10 | 11 | "github.com/theckman/yacspin" 12 | ) 13 | 14 | var ( 15 | buildMode = "prod" 16 | ) 17 | 18 | var lock = &sync.Mutex{} 19 | 20 | type Loader struct { 21 | cfg *yacspin.Config 22 | spinner *yacspin.Spinner 23 | disabled bool 24 | } 25 | 26 | var spinner *Loader 27 | 28 | func GetSpinner() *Loader { 29 | if spinner == nil { 30 | lock.Lock() 31 | defer lock.Unlock() 32 | if spinner == nil { 33 | // NOTE: Don't print the spinner in tests since it causes 34 | // golden files to produce different results. 35 | var cfg yacspin.Config 36 | if buildMode == "TEST" { 37 | cfg = yacspin.Config{ 38 | Frequency: 100 * time.Millisecond, 39 | CharSet: yacspin.CharSets[9], 40 | SuffixAutoColon: false, 41 | Writer: io.Discard, 42 | } 43 | } else { 44 | cfg = yacspin.Config{ 45 | Frequency: 100 * time.Millisecond, 46 | CharSet: yacspin.CharSets[9], 47 | SuffixAutoColon: false, 48 | ShowCursor: true, 49 | } 50 | } 51 | 52 | spn, err := yacspin.New(cfg) 53 | if err != nil { 54 | return nil 55 | } 56 | 57 | spinner = &Loader{ 58 | cfg: &cfg, 59 | spinner: spn, 60 | disabled: false, 61 | } 62 | } 63 | } 64 | 65 | return spinner 66 | } 67 | 68 | func (s *Loader) Start(msg string, delay time.Duration) { 69 | var err error 70 | 71 | s.Enable() 72 | go func() { 73 | time.Sleep(delay * time.Millisecond) 74 | if !s.disabled { 75 | s.spinner.Message(msg) 76 | err = s.spinner.Start() 77 | if err != nil { 78 | return 79 | } 80 | } 81 | }() 82 | 83 | // In-case user interrupts, make sure spinner is stopped 84 | go func() { 85 | sigchan := make(chan os.Signal, 1) 86 | signal.Notify(sigchan, os.Interrupt) 87 | <-sigchan 88 | 89 | if spinner != nil && err == nil { 90 | _ = s.spinner.Stop() 91 | } 92 | os.Exit(0) 93 | }() 94 | } 95 | 96 | func (s *Loader) Stop() { 97 | err := s.spinner.Stop() 98 | if err != nil { 99 | return 100 | } 101 | } 102 | 103 | func (s *Loader) Enable() { 104 | s.disabled = false 105 | } 106 | -------------------------------------------------------------------------------- /core/test/utils.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | 8 | func IsError(t *testing.T, err error) { 9 | if err == nil { 10 | t.Fatalf("%q", err) 11 | } 12 | } 13 | 14 | func CheckErr(t *testing.T, err error) { 15 | if err != nil { 16 | t.Fatalf("%q", err) 17 | } 18 | } 19 | 20 | func WantErr(t *testing.T, err error) { 21 | if err == nil { 22 | t.Fatalf("Wanted error, got nil") 23 | } 24 | } 25 | 26 | func CheckEqS(t *testing.T, found string, wanted string) { 27 | if found != wanted { 28 | t.Fatalf(`Wanted: %q, Found: %q`, wanted, found) 29 | } 30 | } 31 | 32 | func CheckEqN(t *testing.T, found int, wanted int) { 33 | if found != wanted { 34 | t.Fatalf(`Wanted: %d, Found: %d`, wanted, found) 35 | } 36 | } 37 | 38 | // Equal tells whether a and b contain the same elements. 39 | // A nil argument is equivalent to an empty slice. 40 | func CheckEqualStringArr(t *testing.T, found []string, wanted []string) { 41 | equal := true 42 | 43 | if len(found) != len(wanted) { 44 | equal = false 45 | } 46 | for i, v := range found { 47 | if v != wanted[i] { 48 | equal = false 49 | } 50 | } 51 | 52 | if !equal { 53 | t.Fatalf("Wanted: %q, Found: %q", wanted, found[0]) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/utils_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alajmo/sake/core/test" 7 | ) 8 | 9 | func checkEqHost(t *testing.T, hostname string, defaultUser string, defaultPort uint16, wantedHost string, wantedUser string, wantedPort uint16) { 10 | foundUser, foundHost, foundPort, err := ParseHostName(hostname, defaultUser, defaultPort) 11 | test.CheckErr(t, err) 12 | 13 | if foundHost != wantedHost { 14 | t.Fatalf(`Wanted: %q, Found: %q`, wantedHost, foundHost) 15 | } 16 | if foundUser != wantedUser { 17 | t.Fatalf(`Wanted: %q, Found: %q`, wantedUser, foundUser) 18 | } 19 | if foundPort != wantedPort { 20 | t.Fatalf(`Wanted: %q, Found: %q`, wantedPort, foundPort) 21 | } 22 | } 23 | 24 | func TestParseHostName(t *testing.T) { 25 | // IPv4 26 | checkEqHost(t, "192.168.0.1", "test", 33, "192.168.0.1", "test", 33) 27 | checkEqHost(t, "user@192.168.0.1", "test", 33, "192.168.0.1", "user", 33) 28 | checkEqHost(t, "192.168.0.1:44", "test", 33, "192.168.0.1", "test", 44) 29 | checkEqHost(t, "user@192.168.0.1:44", "test", 33, "192.168.0.1", "user", 44) 30 | 31 | // IPv6 32 | checkEqHost(t, "2001:3984:3989::10", "test", 33, "2001:3984:3989::10", "test", 33) 33 | checkEqHost(t, "user@2001:3984:3989::10", "test", 33, "2001:3984:3989::10", "user", 33) 34 | checkEqHost(t, "[2001:3984:3989::10]:44", "test", 33, "2001:3984:3989::10", "test", 44) 35 | checkEqHost(t, "user@[2001:3984:3989::10]:44", "test", 33, "2001:3984:3989::10", "user", 44) 36 | 37 | // Resolved hostname 38 | checkEqHost(t, "resolved", "test", 33, "resolved", "test", 33) 39 | checkEqHost(t, "resolved.com", "test", 33, "resolved.com", "test", 33) 40 | checkEqHost(t, "user@resolved", "test", 33, "resolved", "user", 33) 41 | } 42 | -------------------------------------------------------------------------------- /debug: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "runtime/pprof" 6 | "os" 7 | _ "net/http/pprof" 8 | 9 | "github.com/alajmo/sake/cmd" 10 | ) 11 | 12 | func main() { 13 | fc, err := os.Create("./benchmarks/cpu.prof") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | 18 | fh, err := os.Create("./benchmarks/heap.prof") 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | fg, err := os.Create("./benchmarks/goroutine.prof") 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | pprof.StartCPUProfile(fc) 29 | cmd.Execute() 30 | pprof.Lookup("heap").WriteTo(fh, 0) 31 | pprof.Lookup("goroutine").WriteTo(fg, 0) 32 | defer pprof.StopCPUProfile() 33 | 34 | // go tool pprof -http="localhost:8000" ./benchmarks/cpu.prof 35 | // go tool pprof -http="localhost:8000" ./benchmarks/heap.prof 36 | // go tool pprof -http="localhost:8000" ./benchmarks/goroutine.prof 37 | } 38 | -------------------------------------------------------------------------------- /docs/background.md: -------------------------------------------------------------------------------- 1 | # Background 2 | 3 | > To configure servers, all you need is bash scripting, environment variables, and some know-how. 4 | 5 | `sake` came about because I needed a simple tool to run tasks on remote hosts. There's tons of software in this category, Ansible, pyinfra, Puppet, Chef, Salt, Sup, and probably many more. However, some of them can be quite complex to master, have complicated DSLs and for simple situations are not quite ergonomic. So, `sake` was born, an ergonomic utility CLI tool to run tasks on servers. 6 | 7 | ## Premise 8 | 9 | The premise is you have a bunch of servers and want the following: 10 | 11 | 1. A central place for your servers, containing name, host, and a small description of the servers 12 | 2. Ability to run ad-hoc commands (perhaps `hostnamectl` to see system hostname) on 1, a subset, or all of the servers 13 | 3. Ability to run defined tasks on 1, a subset, or all of the servers 14 | 4. Ability to get an overview of 1, a subset, or all of the servers and tasks 15 | 16 | ## Design 17 | 18 | `sake` prioritizes simplicity and flexibility, principles that are at odds with each other at times: 19 | 20 | 1. Simplicity - both the implementation and the UX is designed to be simple. However, this rule can be bent to some degree if the reward is substantial 21 | 2. Flexibility - provide the user with the ability to shape `sake` to their user case, instead of providing an opinionated setup 22 | 23 | With these principles in mind, I've elected not to create a complex DSL (see Terraform, Puppet, Ansible, etc.), but instead, just add a few primitives and let the user leverage their existing sysadmin knowledge to create their ideal setup. This results in not forcing users to learn yet another DSL, avoiding continuous lookup of `sake` commands, and updating `sake` to get new features. 24 | 25 | It does, however, push complexity onto the user, for instance, there's no built-in primitive to download files and the user must define a task to do so. It would be foolish to aim for feature parity with 3rd party software like rsync for downloading files (there are over 150 flags for rsync). 26 | 27 | So what config format is best suited for this purpose? In my opinion, YAML is a suitable candidate. While it has its issues, I think its purpose as a human-readable config/state file works well. It has all the primitives you'd need in a config language, simple key/value entries, dictionaries, and lists, as well as supporting comments (something which JSON doesn't). We could create a custom format, but then users would have to learn that syntax, so in this case, YAML has a major advantage, many developers are familiar with it. 28 | 29 | I don't intend to introduce any templating capability via Jinja templates or similar, instead, the user will have to leverage shell or any other programming to compose more complex tasks. 30 | 31 | In this sense, `sake` follows the Unix principle: 32 | 33 | - Write programs that do one thing and do it well - run tasks on multiple servers 34 | - Write programs to work together - invoke other programs to do your bidding (rsync) 35 | - Write programs to handle text streams - output of all `sake` commands is just text 36 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions are welcome, be it [filing bugs](https://github.com/alajmo/sake/issues), feature suggestions or helping developing `sake`. 4 | For significant changes, please read [project background](background.md) and open an issue to discuss the changes before starting development. 5 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Build instructions 4 | 5 | ### Prerequisites 6 | 7 | - [go 1.19 or above](https://golang.org/doc/install) 8 | - [goreleaser](https://goreleaser.com/install/) (optional) 9 | - [golangci-lint](https://github.com/golangci/golangci-lint) (optional) 10 | 11 | ### Building 12 | 13 | ```bash 14 | # Build sake for your platform target 15 | make build 16 | 17 | # Build sake binaries and archives for all platforms using goreleaser 18 | make build-all 19 | 20 | # Generate Manpage 21 | make gen-man 22 | ``` 23 | 24 | ## Developing 25 | 26 | ```bash 27 | # Format code 28 | make gofmt 29 | 30 | # Manage dependencies (download/remove unused) 31 | make tidy 32 | 33 | # Lint code 34 | make lint 35 | 36 | # Standing in examples directory you can run the following to debug faster 37 | go run ../main.go run ping -a 38 | ``` 39 | 40 | ## Releasing 41 | 42 | The following workflow is used for releasing a new `sake` version: 43 | 44 | 1. Create pull request with changes 45 | 2. Verify build works (especially windows build) 46 | - `make build` 47 | - `make build-all` 48 | 3. Pass all integration and unit tests locally 49 | - `make integration-test` 50 | - `make unit-test` 51 | 4. Run benchmarks and profiler to check performance 52 | - `make benchmark` 53 | 5. Update `config.md` and `config.man` if any config changes and generate manpage 54 | - `make gen-man` 55 | 6. Update `Makefile` and `CHANGELOG.md` with correct version, and add all changes to `CHANGELOG.md` 56 | 7. Squash-merge to main with `Release vx.y.z` and description of changes 57 | 8. Run `make release`, which will: 58 | - Create a git tag with release notes 59 | - Trigger a build in Github that builds cross-platform binaries and generates release notes of changes between current and previous tag 60 | 61 | ## Overview of How Sake Works 62 | 63 | 1. Parse & validate CLI arguments 64 | 2. Parse `sake` config files and create config, inventory, tasks, specs, and target states 65 | 3. Create clients for remote and local task execution for the selected hosts/tasks 66 | 4. Execute tasks on remote/local hosts 67 | 6. Disconnect from remote hosts 68 | 7. Print any output (results, reports, errors, etc.) 69 | 70 | ## Dependency Graph 71 | 72 | Create SVG dependency graphs using graphviz and [goda](https://github.com/loov/goda). 73 | 74 | ```bash 75 | goda graph "github.com/alajmo/sake/..." | dot -Tsvg -o res/graph.svg 76 | goda graph "github.com/alajmo/sake:all" | dot -Tsvg -o res/graph-full.svg 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | `sake` is available on Linux and Mac: 4 | 5 | * Binaries are available on the [release](https://github.com/alajmo/sake/releases) page 6 | 7 | * via cURL 8 | ```bash 9 | curl -sfL https://raw.githubusercontent.com/alajmo/sake/main/install.sh | sh 10 | ``` 11 | 12 | * via Homebrew 13 | ```bash 14 | brew tap alajmo/sake 15 | brew install sake 16 | ``` 17 | 18 | * via MacPorts 19 | ```sh 20 | sudo port install sake 21 | ``` 22 | 23 | * via Arch 24 | ```sh 25 | pacman -S sake 26 | ``` 27 | 28 | * via pkg 29 | ```sh 30 | pkg install sake 31 | ``` 32 | 33 | * via Go 34 | ```bash 35 | go install github.com/alajmo/sake@latest 36 | ``` 37 | 38 | ## Building From Source 39 | 40 | Requires [go 1.19 or above](https://golang.org/doc/install). 41 | 42 | 1. Clone the repo 43 | 2. Build and run the executable 44 | 45 | ```bash 46 | make build && ./dist/sake 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: / 3 | --- 4 | 5 | # Introduction 6 | 7 | `sake` is a command runner for local and remote hosts. You define servers and tasks in a `sake.yaml` config file and then run the tasks on the servers. 8 | 9 | `sake` has tons of features: 10 | 11 | - auto-completion of tasks, servers and tags 12 | - SSH into servers or docker containers `sake ssh ` 13 | - list servers/tasks via `sake list servers|tasks` 14 | - present task output in a compact table format `sake run --output table` 15 | - open task/server in your preferred editor `sake edit task ` 16 | - import other `sake.yaml` configs 17 | - and [many more!](recipes.md) 18 | 19 | ## Demo 20 | 21 | ![demo](/img/output.gif) 22 | 23 | ## Example 24 | 25 | You specify servers and tasks in a config file: 26 | 27 | ```yaml title=sake.yaml 28 | servers: 29 | localhost: 30 | desc: my workstation 31 | host: localhost 32 | local: true 33 | tags: [local] 34 | 35 | server-1: 36 | desc: hosts mealie, Node-RED 37 | host: server-1.lan 38 | user: samir 39 | tags: [remote,pi] 40 | 41 | pihole: 42 | desc: runs pihole 43 | host: pihole.lan 44 | user: samir 45 | tags: [remote,pi] 46 | 47 | tasks: 48 | ping: 49 | desc: ping server 50 | cmd: echo pong 51 | 52 | print-host: 53 | name: Host 54 | desc: print host 55 | cmd: echo $S_HOST 56 | 57 | print-os: 58 | name: OS 59 | desc: print OS 60 | cmd: | 61 | os=$(lsb_release -si) 62 | release=$(lsb_release -sr) 63 | echo "$os $release" 64 | 65 | print-kernel: 66 | name: Kernel 67 | desc: Print kernel version 68 | cmd: uname -r | awk -v FS='-' '{print $1}' 69 | 70 | info: 71 | desc: get remote info 72 | target: 73 | tags: [remote] 74 | spec: 75 | output: table 76 | strategy: free 77 | ignore_errors: true 78 | ignore_unreachable: true 79 | tasks: 80 | - task: print-os 81 | - task: print-kernel 82 | ``` 83 | 84 | and then run the tasks over all or a subset of the repositories: 85 | 86 | ```bash 87 | # Simple ping command 88 | $ sake run ping --tags remote 89 | 90 | TASK [ping: ping server] ************** 91 | 92 | server-1.lan | pong 93 | 94 | TASK [ping: ping server] ************** 95 | 96 | pihole.lan | pong 97 | 98 | # Multiple tasks 99 | $ sake run info --all 100 | 101 | Server | OS | Kernel 102 | ----------+-----------+--------- 103 | server-1 | Debian 11 | 5.10.92 104 | pihole | Debian 11 | 5.10.92 105 | 106 | # Runtime defined command 107 | sake exec 'sudo apt install rsync' --tags remote 108 | 109 | TASK Command ****************************************************************** 110 | 111 | server-1.lan | Reading package lists... 112 | server-1.lan | Building dependency tree... 113 | server-1.lan | Reading state information... 114 | server-1.lan | rsync is already the newest version (3.2.3-4+deb11u1). 115 | server-1.lan | 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. 116 | 117 | TASK Command ****************************************************************** 118 | 119 | pihole.lan | Reading package lists... 120 | pihole.lan | Building dependency tree... 121 | pihole.lan | Reading state information... 122 | pihole.lan | rsync is already the newest version (3.2.3-4+deb11u1). 123 | pihole.lan | 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. 124 | 125 | $ sake ssh server-1 126 | samir@server-1:~ $ 127 | ``` 128 | -------------------------------------------------------------------------------- /docs/inventory.md: -------------------------------------------------------------------------------- 1 | # Inventory 2 | 3 | Inventory is a collection of hosts that you can execute tasks on. There are 4 ways to specify hosts: 4 | 5 | ```yaml 6 | servers: 7 | # Single Host 8 | single-1: 9 | host: 192.168.1.1 10 | user: test 11 | port: 33 12 | bastion: test@192.168.0.1:22 13 | tags: [web] 14 | 15 | # Multipe hosts using a list 16 | many-1: 17 | hosts: 18 | - test@192.168.1.1:22 19 | - test@192.168.1.2:22 20 | tags: [web, prod] 21 | 22 | # Multiple hosts using a ranges 23 | many-2: 24 | hosts: test@192.168.1.[1:2]:22 25 | tags: [web, prod] 26 | 27 | # Multiple hosts by invoking a shell command 28 | many-3: 29 | inventory: echo test@192.168.1.1:22 test@192.168.1.2:22 30 | tags: [web, prod] 31 | ``` 32 | 33 | To target the hosts in a task there's multiple ways: 34 | 35 | - **all**: target 36 | - **servers**: a list of single hosts or group of hosts 37 | - supports range as well, for instance `--server "list[0:1]"`, select first and second host 38 | - **tags**: target hosts that have a specific tag 39 | - **regex**: target hosts on host regex 40 | - **invert**: invert matching on hosts 41 | 42 | Furthermore, to limit the number of targetted servers, you can use one of following properties: 43 | 44 | - **limit**: limit the number of targetted hosts 45 | - **limit_p**: limit the number of targetted hosts in percentage 46 | 47 | ## Provide Identity and Password Credentials 48 | 49 | By default `sake` will attempt to load identity keys from an SSH agent if it's running in the background. However, if you wish to provide credentials manually, you can do so by (first takes precedence): 50 | 51 | 1. setting `--identity-file` and/or `--password` flags 52 | 2. specifying it in the server definition 53 | 54 | The type of auth used is determined by: 55 | 56 | - if `identity-file` and `password` are provided, then it assumes password protected identity key 57 | - if only `identity-file` is provided, then it first tries without passphrase, if file is encrypted, it will prompt for passphrase 58 | - if only `password` is provided, then it assumes password protected auth 59 | 60 | ```yaml 61 | servers: 62 | server-1: 63 | host: server-1.lan 64 | identity_file: id_rsa 65 | password: $(echo $MY_SECRET_PASSWORD) 66 | ``` 67 | 68 | You can also define entries in your `~/.ssh/config` file and `sake` will try to resolve them. 69 | 70 | ## Known Hosts 71 | 72 | By default a `known_hosts` file is used to verify host connections. If you wish to disable verification, set the global property `disable_verify_host` to true: 73 | 74 | ```yaml 75 | disable_verify_host: true 76 | ``` 77 | 78 | The default location of the known hosts file is `$HOME/.ssh/known_hosts`. If you wish change this to another file, then set the global property `known_hosts_file` to your desired filepath: 79 | 80 | ```yaml 81 | known_hosts_file: ./known_hosts 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/man-pages.md: -------------------------------------------------------------------------------- 1 | # Man Page 2 | 3 | Man page generation is available via: 4 | 5 | ```bash 6 | $ sake gen 7 | Created sake.1 8 | 9 | # Or specify a different directory 10 | $ sake gen --dir /usr/local/share/man/man1/ 11 | Created /usr/local/share/man/man1/sake.1 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | `sake` is under active development. Before **v1.0.0**, I want to finish the following tasks, some miscellaneous fixes, improve code documentation and refactor: 4 | 5 | - [ ] Task not callable, only from another task (as not to accidently call it) 6 | - [ ] Hide tasks from auto-completion via `hidden: true` attribute 7 | - [ ] Silent output from task via `silent: true` (and flag) 8 | - [ ] ExecTTY should be config shell 9 | - [ ] Add flag `default_timeout_s` 10 | - [ ] Use `chdir` for tasks, `work_dir` for servers 11 | - [ ] Move limit/limitp to spec, or move order to target 12 | - [ ] Figure out changed/skipped/when 13 | - [ ] Conditional tasks (success, error, skip) 14 | - [ ] Add callbacks (success/error) 15 | - [ ] Loader show current task and how many left on table 16 | - [ ] Add retries to task 17 | - [ ] Add required envs 18 | - [ ] Add option to prompt for envs 19 | - [ ] Handle `Match *` in ssh config for inventory as well 20 | - [ ] Something similar to play, to trigger multiple tasks (with their own context) 21 | - [ ] Add env variables to multiple servers 22 | - [ ] Run one task, save output from all, and then have one task handle differences 23 | - [ ] Save logs/output to files (remote/local) 24 | - [ ] Diff task 25 | - [ ] Inherit default from `default` spec/target 26 | - [ ] Add yaml to command mapper 27 | - [ ] Implement facts 28 | - [ ] Configure what to show, host/ip or name, configure via theme flags 29 | - [x] Template for server prefix, similar to header 30 | - [ ] Add colors to describe (key bold, value color), true (green), false (red) 31 | - [ ] Add Tree output 32 | - [ ] Fix hashed ip6 with port 22 does not work, all other combinations work 33 | - [ ] Fix `sake ssh inv` not working 34 | -------------------------------------------------------------------------------- /docs/shell-completion.mdx: -------------------------------------------------------------------------------- 1 | # Shell Completion 2 | 3 | Shell completion is available for bash, zsh and fish. 4 | 5 | import Tabs from '@theme/Tabs'; 6 | import TabItem from '@theme/TabItem'; 7 | 8 | 17 | 18 | 19 | 20 | ```bash 21 | sake completion bash 22 | ``` 23 | 24 | 25 | 26 | 27 | 28 | ```bash 29 | sake completion zsh 30 | ``` 31 | 32 | 33 | 34 | 35 | 36 | ```bash 37 | sake completion fish 38 | ``` 39 | 40 | 41 | 42 | 43 | 44 | ```bash 45 | sake completion powershell 46 | ``` 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/task-execution.md: -------------------------------------------------------------------------------- 1 | # Task Execution 2 | 3 | Sake offers multiple execution strategies that controls how tasks are executed across the hosts. The available ones are: 4 | 5 | - **linear**: execute task for each host before proceeding to the next task (default) 6 | - **host_pinned**: executes tasks (serial) for a host before proceeding to the next host 7 | - **free**: executes tasks without waiting for other tasks 8 | 9 | You can set the strategy via the `stragegy` property in a `spec` definition or via a flag `--strategy [option]`. 10 | 11 | Additionally, the following properties are available to further control task execution: 12 | 13 | - **batch**: number of hosts to run in parallel 14 | - **batch_p**: percentage of hosts to run in parallel 15 | - **forks**: max number of concurrent processes 16 | 17 | ## Linear Strategy 18 | 19 | When the following properties are set: 20 | 21 | - **strategy**: linear 22 | - **batch**: 2 23 | - **forks**: 10000 24 | 25 | Sake will execute according to the following image: 26 | 27 | ![linear](/img/linear-strategy.png) 28 | 29 | 1. Task `T1` will run in parallel for hosts `H1` and `H2` 30 | 2. Task `T1` will run for host `H3` 31 | 3. Task `T2` will run in parallel for hosts `H1` and `H2` 32 | 4. Task `T2` will run for host `H3` 33 | 5. Task `T3` will run in parallel for hosts `H1` and `H2` 34 | 6. Task `T3` will run for host `H3` 35 | 36 | ## Host Pinned Strategy 37 | 38 | When the following properties are set: 39 | 40 | - **strategy**: host_pinned 41 | - **batch**: 2 42 | - **forks**: 10000 43 | 44 | Sake will execute according to the following image: 45 | 46 | ![linear](/img/host_pinned-strategy.png) 47 | 48 | 1. Tasks `T1 - T3` will run serially for `H1` and `H2` (in parallel) 49 | 2. Tasks `T1 - T3` will run serially for `H3` 50 | 51 | ## Free Strategy 52 | 53 | When the following properties are set: 54 | 55 | - **strategy**: free 56 | - **batch_p**: 100% 57 | - **forks**: 10000 58 | 59 | Sake will execute according to the following image: 60 | 61 | ![linear](/img/free-strategy.png) 62 | 63 | 1. All tasks for all hosts will run in parallel 64 | 65 | ## Ordering Hosts 66 | 67 | There are multiple host ordering options available: 68 | 69 | - **inventory**: the order is as provided by the inventory 70 | - **reverse_inventory**: the order is the reverse of the inventory 71 | - **sorted**: hosts are alphabetically sorted by host 72 | - **reverse_sorted**: hosts are sorted by host in reverse alphabetical order 73 | - **random**: hosts are randomly ordered 74 | 75 | ## Confirm Before Running Tasks 76 | 77 | sake comes with two options to confirm tasks before execution: 78 | 79 | - **confirm**: this property is used when you want to simply confirm the task you invoked before running 80 | - it can be specified either via flag `--confirm` or the spec property `confirm` 81 | - **step**: this property is used when you want to confirm each task individually 82 | - it can be specified either via flag `--step` or the spec property `step` 83 | - Invoking `--step` will provide the user 3 options: 84 | - `yes`: run the task 85 | - `no`: skip the task 86 | - `continue`: run the task and don't prompt for the next tasks 87 | 88 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Create a New Sake Config 4 | 5 | Run the following command: 6 | 7 | ```bash 8 | $ sake init 9 | 10 | Initialized sake in /tmp/sake 11 | - Created sake.yaml 12 | 13 | Following servers were added to sake.yaml 14 | 15 | Server | Host 16 | -----------+--------- 17 | localhost | 0.0.0.0 18 | ``` 19 | 20 | Our `sake.yaml` config file should look like this: 21 | 22 | ```yaml title=sake.yaml 23 | servers: 24 | localhost: 25 | host: 0.0.0.0 26 | local: true 27 | 28 | tasks: 29 | ping: 30 | desc: Pong 31 | cmd: echo "pong" 32 | "``` 33 | 34 | ## Run Some Commands 35 | 36 | Now let's run some commands to see everything is working as expected. 37 | 38 | ```bash 39 | # List all servers 40 | $ sake list servers 41 | 42 | Server | Host 43 | -----------+--------- 44 | localhost | 0.0.0.0 45 | 46 | # List all tasks 47 | $ sake list tasks 48 | 49 | Task | Description 50 | ------+------------- 51 | ping | Pong 52 | 53 | # Run Task 54 | $ sake run ping --all 55 | 56 | TASK ping: Pong ************ 57 | 58 | 0.0.0.0 | pong 59 | 60 | # Count number of files in each servers in parallel 61 | $ sake exec --all --output table --strategy=free 'find . -type f | wc -l' 62 | 63 | Server | Output 64 | -----------+-------- 65 | localhost | 1 66 | ``` 67 | 68 | Next up: 69 | 70 | - [Simple examples](/examples) 71 | - [Recipes](/recipes) 72 | - [Config Reference](/config-reference) 73 | - [Command Reference](/command-reference) 74 | -------------------------------------------------------------------------------- /docs/variables.md: -------------------------------------------------------------------------------- 1 | # Variables 2 | 3 | sake supports setting variables for both servers and tasks. The variable can either be a string or a command (in which case it's encapsulated by `$()`) which will be evaluated (once) for each task. 4 | 5 | ```yaml 6 | servers: 7 | webserver: 8 | host: 172.1.2.3 9 | env: 10 | string: hello world 11 | 12 | tasks: 13 | ping: 14 | cmd: echo "$msg" 15 | env: 16 | msg: pong 17 | date: $(date) 18 | ``` 19 | 20 | Additionally, the following environment variables are available by default for all tasks: 21 | 22 | - Server specific: 23 | - `S_NAME` 24 | - `S_HOST` 25 | - `S_USER` 26 | - `S_PORT` 27 | - `S_TAGS` 28 | 29 | - Config specific: 30 | - `SAKE_DIR` 31 | - `SAKE_PATH` 32 | - `SAKE_KNOWN_HOSTS_FILE` 33 | 34 | ## Pass Variables from CLI 35 | 36 | To pass variables from the CLI prompt, simply pass an argument, for instance: 37 | 38 | ```bash 39 | sake run msg option=123 40 | ``` 41 | 42 | Now the environment variable `option` can be used in the task. 43 | 44 | ## Register Variables in Tasks 45 | 46 | To access a previous tasks output, you can register a variable in the previous task, which will be available as an environment variable in the current task. In addition to just capturing output, the following environment variables will be available: 47 | 48 | - `_status`: 49 | - `_rc`: 50 | - `_failed`: 51 | - `_stdout`: 52 | - `_stderr`: 53 | 54 | ```yaml 55 | tasks: 56 | ping: 57 | tasks: 58 | - cmd: echo "foo" && >&2 echo "error" 59 | register: out 60 | 61 | - cmd: | 62 | echo "status: $out_status" 63 | echo "rc: $out_rc" 64 | echo "failed: $out_failed" 65 | echo "stdout: $out_stdout" 66 | echo "stderr: $out_stderr" 67 | echo "out:" 68 | echo "$out" 69 | ``` 70 | 71 | Output: 72 | 73 | ```bash 74 | $ sake run ping 75 | 76 | TASKS ****************************** 77 | 78 | TASK (1/2) [task-0] **************** 79 | 80 | 172.24.2.2 | error 81 | 172.24.2.2 | foo 82 | 83 | TASK (2/2) [task-1] **************** 84 | 85 | 172.24.2.2 | status: ok 86 | 172.24.2.2 | rc: 0 87 | 172.24.2.2 | failed: false 88 | 172.24.2.2 | stdout: foo 89 | 172.24.2.2 | stderr: error 90 | 172.24.2.2 | out: 91 | 172.24.2.2 | error 92 | 172.24.2.2 | foo 93 | ``` 94 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alajmo/sake 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/gobwas/glob v0.2.3 7 | github.com/jedib0t/go-pretty/v6 v6.6.5 8 | github.com/kevinburke/ssh_config v1.2.0 9 | github.com/kr/pretty v0.2.1 10 | github.com/spf13/cobra v1.8.1 11 | github.com/spf13/pflag v1.0.5 12 | github.com/theckman/yacspin v0.13.12 13 | golang.org/x/crypto v0.31.0 14 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 15 | golang.org/x/sys v0.28.0 16 | golang.org/x/term v0.27.0 17 | gopkg.in/yaml.v3 v3.0.1 18 | ) 19 | 20 | require ( 21 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 22 | github.com/fatih/color v1.18.0 // indirect 23 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 24 | github.com/kr/text v0.1.0 // indirect 25 | github.com/mattn/go-colorable v0.1.13 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/mattn/go-runewidth v0.0.16 // indirect 28 | github.com/rivo/uniseg v0.4.7 // indirect 29 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 30 | golang.org/x/text v0.21.0 // indirect 31 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /img/cpu-1.csv: -------------------------------------------------------------------------------- 1 | name,sake,pyinfra,ansible 2 | 1,8,90,86 3 | 3,16,86,96 4 | 5,32,88,106 5 | 8,31,88,118 6 | 10,40,92,125 7 | 25,41,97,151 8 | 50,50,102,267 9 | 100,47,111,288 10 | 200,48,126,284 11 | 300,50,136,270 12 | 400,50,144,262 13 | 500,48,151,254 14 | -------------------------------------------------------------------------------- /img/cpu-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/cpu-1.png -------------------------------------------------------------------------------- /img/cpu-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/cpu-2.png -------------------------------------------------------------------------------- /img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/docusaurus.png -------------------------------------------------------------------------------- /img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/favicon.ico -------------------------------------------------------------------------------- /img/free-strategy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/free-strategy.png -------------------------------------------------------------------------------- /img/host_pinned-strategy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/host_pinned-strategy.png -------------------------------------------------------------------------------- /img/linear-strategy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/linear-strategy.png -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/logo.png -------------------------------------------------------------------------------- /img/mem-1.csv: -------------------------------------------------------------------------------- 1 | name,sake,pyinfra,ansible 2 | 1,15,57,54 3 | 3,17,56,54 4 | 5,17,58,54 5 | 8,21,58,54 6 | 10,21,59,54 7 | 25,23,59,57 8 | 50,23,60,57 9 | 100,27,65,56 10 | 200,33,69,60 11 | 300,40,71,67 12 | 400,46,76,67 13 | 500,51,80,73 14 | -------------------------------------------------------------------------------- /img/mem-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/mem-1.png -------------------------------------------------------------------------------- /img/mem-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/mem-2.png -------------------------------------------------------------------------------- /img/output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/output.gif -------------------------------------------------------------------------------- /img/time-1-short.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/time-1-short.png -------------------------------------------------------------------------------- /img/time-1.csv: -------------------------------------------------------------------------------- 1 | name,sake,pyinfra,ansible 2 | 1,0.085,0.915,0.620 3 | 3,0.135,0.950,0.650 4 | 5,0.140,0.965,0.690 5 | 8,0.130,0.980,0.720 6 | 10,0.170,1.010,0.750 7 | 25,0.150,1.135,1.170 8 | 50,0.200,1.385,2.210 9 | 100,0.315,1.935,4.420 10 | 200,0.540,2.925,8.530 11 | 300,0.785,4.225,11.860 12 | 400,1.045,5.290,15.400 13 | 500,1.330,6.510,18.320 14 | -------------------------------------------------------------------------------- /img/time-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/time-1.png -------------------------------------------------------------------------------- /img/time-2-short.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/time-2-short.png -------------------------------------------------------------------------------- /img/time-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/img/time-2.png -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Credit to https://github.com/ducaale/xh for this install script. 3 | 4 | set -e 5 | 6 | if [ "$(uname -s)" = "Darwin" ] && [ "$(uname -m)" = "x86_64" ]; then 7 | target="darwin_amd64" 8 | elif [ "$(uname -s)" = "Darmin" ] && [ "$(uname -m)" = "arm64" ]; then 9 | target="darmin_arm64" 10 | elif [ "$(uname -s)" = "Linux" ] && [ "$(uname -m)" = "x86_64" ]; then 11 | target="linux_amd64" 12 | elif [ "$(uname -s)" = "Linux" ] && ( uname -m | grep -q -e '^arm' -e '^aarch' ); then 13 | target="linux_arm64" 14 | else 15 | echo "Unsupported OS or architecture" 16 | exit 1 17 | fi 18 | 19 | fetch() { 20 | if which curl > /dev/null; then 21 | if [ "$#" -eq 2 ]; then curl -L -o "$1" "$2"; else curl -sSL "$1"; fi 22 | elif which wget > /dev/null; then 23 | if [ "$#" -eq 2 ]; then wget -O "$1" "$2"; else wget -nv -O - "$1"; fi 24 | else 25 | echo "Can't find curl or wget, can't download package" 26 | exit 1 27 | fi 28 | } 29 | 30 | echo "Detected target: $target" 31 | 32 | url=$( 33 | fetch https://api.github.com/repos/alajmo/sake/releases/latest | 34 | tac | tac | grep -wo -m1 "https://.*$target.tar.gz" || true 35 | ) 36 | 37 | if ! test "$url"; then 38 | echo "Could not find release info" 39 | exit 1 40 | fi 41 | 42 | echo "Downloading sake..." 43 | 44 | temp_dir=$(mktemp -dt sake.XXXXXX) 45 | trap 'rm -rf "$temp_dir"' EXIT INT TERM 46 | cd "$temp_dir" 47 | 48 | if ! fetch sake.tar.gz "$url"; then 49 | echo "Could not download tarball" 50 | exit 1 51 | fi 52 | 53 | user_bin="$HOME/.local/bin" 54 | case $PATH in 55 | *:"$user_bin":* | "$user_bin":* | *:"$user_bin") 56 | default_bin=$user_bin 57 | ;; 58 | *) 59 | default_bin='/usr/local/bin' 60 | ;; 61 | esac 62 | 63 | printf "Install location [default: %s]: " "$default_bin" 64 | read -r bindir < /dev/tty 65 | bindir=${bindir:-$default_bin} 66 | 67 | while ! test -d "$bindir"; do 68 | echo "Directory $bindir does not exist" 69 | printf "Install location [default: %s]: " "$default_bin" 70 | read -r bindir < /dev/tty 71 | bindir=${bindir:-$default_bin} 72 | done 73 | 74 | tar xzf sake.tar.gz 75 | 76 | if test -w "$bindir"; then 77 | mv sake "$bindir/" 78 | else 79 | sudo mv sake "$bindir/" 80 | fi 81 | 82 | $bindir/sake --version 83 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/alajmo/sake/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /res/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/res/logo.png -------------------------------------------------------------------------------- /res/output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/res/output.gif -------------------------------------------------------------------------------- /scripts/gif.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eum 4 | 5 | SAKE_PATH=$(dirname $(dirname $(realpath "$0"))) 6 | SAKE_EXAMPLE_PATH="./test/playground" 7 | OUTPUT_FILE="$SAKE_PATH/res/output.json" 8 | OUTPUT_GIF="$SAKE_PATH/res/output.gif" 9 | 10 | _init() { 11 | cd "$SAKE_EXAMPLE_PATH" 12 | 13 | # remove previous artifacts 14 | rm "$OUTPUT_FILE" "$OUTPUT_GIF" -f 15 | } 16 | 17 | _simulate_commands() { 18 | # list of the commands we want to record 19 | local CMD=' 20 | _mock() { 21 | # the | pv -qL 30 part is used to simulate typing 22 | echo "\$ $1" | pv -qL 40 23 | 24 | first_char=$(printf %.1s "$1") 25 | if test "$first_char" != "'#'"; then 26 | $1 27 | fi 28 | } 29 | 30 | clear 31 | export PS1="\$ " 32 | sleep 1s 33 | 34 | _mock "# Ping all hosts one by one" 35 | sleep 0.5s 36 | _mock "sake run ping --tags remote" 37 | sleep 1.3s 38 | clear 39 | 40 | _mock "# Ping all hosts in parallel" 41 | sleep 0.5s 42 | _mock "sake run ping --tags remote --parallel" 43 | sleep 1.3s 44 | clear 45 | 46 | sleep 0.5s 47 | _mock "# Query servers for some stats" 48 | sleep 1s 49 | _mock "sake run info --all --output table" 50 | sleep 2s 51 | clear 52 | ' 53 | 54 | asciinema rec -c "$CMD" --idle-time-limit 100 --title mani --quiet "$OUTPUT_FILE" & 55 | fg %1 56 | } 57 | 58 | _generate_gif() { 59 | cd "$SAKE_PATH/res" 60 | 61 | # Convert to gif 62 | output_file=$(basename $OUTPUT_FILE) 63 | output_gif=$(basename $OUTPUT_GIF) 64 | docker run --rm -v "$PWD":/data asciinema/asciicast2gif -S 3 -h 30 "$output_file" "$output_gif" 65 | } 66 | 67 | _main() { 68 | _init 69 | _simulate_commands 70 | _generate_gif 71 | } 72 | 73 | _main 74 | 75 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # Get latest version changes only 6 | sed '0,/## v/d;/## v/Q' docs/changelog.md | tail -n +2 | head -n-1 > release-changelog.md 7 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | RUN apt-get update && apt-get install openssh-server vim sudo -y 4 | 5 | RUN useradd --user-group --system --create-home -s /bin/bash --no-log-init test 6 | 7 | RUN echo 'test:test' | chpasswd 8 | 9 | USER test 10 | 11 | RUN mkdir /home/test/.ssh && touch /home/test/.ssh 12 | 13 | COPY --chown=test:test ./authorized_keys /home/test/.ssh/authorized_keys 14 | 15 | USER root 16 | 17 | RUN usermod -aG sudo test 18 | RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers 19 | RUN echo 'MaxStartups 100' >> /etc/ssh/sshd_config 20 | 21 | RUN service ssh start 22 | 23 | WORKDIR /home/test 24 | 25 | ENTRYPOINT [ "/usr/sbin/sshd", "-D" ] 26 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | `sake` relies on unit tests and integration tests. The integration tests require `docker` to run, since we need to mock SSH connections somehow, and this is the easiest ways to go about it, without creating mock interfaces. The integration tests uses `golden` files to verify the output of `sake` commands. 4 | `sake` also has a few profile tests which measures execution time of various commands. 5 | 6 | The Docker Compose files has multiple servers with different auths (pem, rfc, ed, rsa, encrypted, unencrypted, etc.) and ip addresses (ipv4/6). 7 | 8 | ## Profiling 9 | 10 | To run benchmark run `make benchmark`, and to save benchmarks run `make benchmark-save`. 11 | 12 | Additionally, if you wish to dive into cpu, heap, and goroutine profiling, replace the `main.go` with the contents of `debug.go`, cd into test directory and run: 13 | 14 | ```sh 15 | go run ../main.go run ping -t reachable 16 | go tool pprof -http="localhost:8000" ./benchmarks/cpu.prof 17 | go tool pprof -http="localhost:8000" ./benchmarks/heap.prof 18 | go tool pprof -http="localhost:8000" ./benchmarks/goroutine.prof 19 | ``` 20 | 21 | ## Unit Tests 22 | 23 | To run unit tests run `make unit-test`. 24 | 25 | ## Integration 26 | 27 | For local testing first start docker compose in the background so we can have some servers to test against: 28 | 29 | ```bash 30 | make mock-ssh 31 | ``` 32 | 33 | Then afterward run the tests: 34 | 35 | ```bash 36 | make integration-test 37 | ``` 38 | 39 | To update the `golden` files run: 40 | 41 | ```bash 42 | make update-golden-files 43 | ``` 44 | 45 | ### Generating Keys 46 | 47 | We generate multiple keys for testing, both encrypted and unencrypted identity keys. The key types tested are **rsa** and **ed25519**, and the key formats used are **RFC 4716** and **PEM**. 48 | We also have servers accepting only user and passwords. 49 | 50 | The user credentials for private keys are: 51 | 52 | - user: test 53 | - password: testing 54 | 55 | The user credentials for password auth is: 56 | 57 | - user: test 58 | - password: test 59 | 60 | The public keys are then mounted to `/home/test/.ssh/authorized_keys` via the local file `test/authorized_keys` by using Docker volume. 61 | 62 | Stand in the keys directory and run the following commands: 63 | 64 | ```bash 65 | # Encrypted + RSA + RFC 66 | ssh-keygen -t rsa -f id_rsa_rfc -C "test@test.test" -N "testing" 67 | 68 | # Encrypted + RSA + PEM 69 | ssh-keygen -t rsa -f id_rsa_pem -C "test@test.test" -N "testing" -m PEM 70 | 71 | # Encrypted + ed25519 + RFC 72 | ssh-keygen -t ed25519 -f id_ed25519_rfc -C "test@test.test" -N "testing" 73 | 74 | # Encrypted + ed25519 + PEM 75 | ssh-keygen -t ed25519 -f id_ed25519_pem -C "test@test.test" -N "testing" -m PEM 76 | 77 | # Unencrypted + RSA + RFC 78 | ssh-keygen -t rsa -f id_rsa_rfc_no -C "test@test.test" -N "" 79 | 80 | # Unencrypted + RSA + PEM 81 | ssh-keygen -t rsa -f id_rsa_pem_no -C "test@test.test" -N "" -m PEM 82 | 83 | # Unencrypted + ed25519 + RFC 84 | ssh-keygen -t ed25519 -f id_ed25519_rfc_no -C "test@test.test" -N "" 85 | 86 | # Unencrypted + ed25519 + PEM 87 | ssh-keygen -t ed25519 -f id_ed25519_pem_no -C "test@test.test" -N "" -m PEM 88 | ``` 89 | -------------------------------------------------------------------------------- /test/authorized_keys: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPdSXo09a9pJE7GZ2Hlj6ZtMF16kmpwrzBjWna6GFMzZ test@test.test 2 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjc3MHhjEA5XAD6hGOiG85n8G8Sq1GUVR56agYh0Vga test@test.test 3 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9qCz2Ioueqf4ad/uzSh73PQ9NtIxxl800MI6XZMthX test@test.test 4 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIELBYV8Hc94RMiAPFpP1ESpcM+duMRqkjC38eT7e0jKg test@test.test 5 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQChS2wcCaC7PP9YsjDdw7ZYOKefER7f5ve3S0tuDWvuDNJMKt+zzyFILxLLWL9on/H4SaUN6u1REzP82/buH20t6Id+32WQwsO99dm0oANfA6EUrmMIIV5rwMj+FIJl75t73eaHKoj1Kx9zqflmtQNxtBy9rwRI7PqBFHaSskqVQT0QF8YX03VE1Mfc5Do0XgaY1v7ayR2WDO3oJhPGQLQn66ooy5dtScCHYRQdXPNDZV9QybOxPD6UZSWOmTjiQicLLxDdjhkOD6moxZHdmTTvUnPVg8o0dimHk0N7YQquoXntVWY6tdJuzdPufgtAayNlvT1vgrHj3cwEU9XtMzoXhoqcFvt0uYh/LCDtaUdgVxpknbjN9r/8d8BuKPIQ9uxP71ZqTvonZwHdr+aqaieS2oaOEAUtwflgU2ZreBKYlw4GYv1DMyul4xf//SYMfIwJOXNRelk0brNUW/WZJPbLi+25NqAmTNyxE1MjiTG3dCIKgIP63jl5Y/0EEvG9k1s= test@test.test 6 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDqWyIZv7SvtgnK8YdfTeMsygYe8n6AkOQaZuGM+v6XmMFq5ALI+PCk4T5UwzpQH6+aFIQ0iCFXyJ7WhBgKurX9vP3F51Ul9zI5KBZ91PCwd8KVFRUZEBZ2w7xm1EuFqa1rxG9F9lcoYw33+c3mm6R8zmqdrXV2e9FLiTAvfTX4STYhPx1z6SOxzNz9rgGZdL3PoWgYOZfDgWeb4421I46klPbtQ0dwJB8+w6nblJbZVubkXoTYsYtcFrxR//ibKxTPxqgXNSt1Wd3ePdd38qgAnDlMUgpUGjx6wylRky97VA+6at9yWhsBwcaxvkWNgXbGpML05izg8bZMAzmcUCqIWfrQqTOqNxGCftpdbpgIpJw5QmV5kueFNvBqAcs98rW2rBVhhJWT6mLXcoxvy3WAbjYQJZrxFJd9kHU/1WVO8F2vjEZObYEOTLVCf1+xdeD1XwJB5eo6ppZi3xtT7YsZkjqhBjkqH4f2dVvUQtunY7BDYR9FbwQswZXnGnHjb5k= test@test.test 7 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC+nW+SA41wGn0064WZ8xY7ImMyEYY0yv0ySRMvnyQcvQ179EqLxrDSpqudXBrUYfo5BOzKGgemhbHL9guSboEnEYDmSZu+EHf7CILVVcERrnPXAulVq+5BcrqfIJQ0x+NAfWWYegk7w3C02YV2FtLFJwrjiQb5at+wSQPPt+lo5Dc2pOZ/mv3k5GEe0po8Ou1gSX9zzyTLfpHMDMQIuESynuLJ7Zc9oZeWF9lDKKyyk43WtqYAWkCU4dxTD6ZGV48PgDpg39dZz7B1GQvOxzRNNGUuiKLCdOmkarF7TdcoKpTLn4bVofUFhlWuvSGffoaz/AlKbJ9sFWCRCY/hMY4NtxvMfYMSlI9xY5KGxKvgL0N0UXTayp/lL5jK0vIkxnArQNmPfZw4dEqxcITJq65s2ZaqCOyHilhoyjvcAeexU/PkpGxqdLW6gK6z5eUBA78P+j3n88o1BTb3YJUPcmCqjEZc5bmxchG4NNzueyM/9WPRP0E4jVH0FapeUEUS4jE= test@test.test 8 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDUu7XdxWcTqd7h+QAQFc3atFm/qt2FBEA+PwM+m3LRwgMy2rncVcg+w7g+GNTG7mqTjrbZQb8Dq7hS5RJRwdZkW+SOI3LF2pE3eo6bhtHNx9vHf6KIDL7k/6qQgHTjsKS5AGD2Gg44tEC/nyeNmdlMkO/0kqegEBWqSVh8P3SPm/nthTSKwPR5R8v5WfiBoiEgXbKSD78L5uRO5Ieu4FPL37T2wclgL2tNVQcIbSVldYYK9lzMVdf+W/Hxca2ZW4qCl3KtUORTKu0qIo17yDSjf0hG/yxZ6AMK5pqqg3ZILo9Z0+x0lS+/FAocOXW48BFHupaheIsQfOfjoIE1E5n24ugCxzgYrL3ooxdHUkSeon+5R9TQSTcE2j3o7rBOWNu5YVtTug87kz5SiYC+BvutAiH7EF69h/+4lfUn8QtWKL3DGCNHqvqz2ITF76b8oJUPTvHoksFBDRl/+78z4DyehZ4c6bRCYMI+ZXp6oYISO0pAsJKnc+okjOOF8MEifZc= test@test.test 9 | 10 | -------------------------------------------------------------------------------- /test/benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | function parse_options() { 6 | SAVE= 7 | while [[ $# -gt 0 ]]; do 8 | case "${1}" in 9 | --save|-s) 10 | SAVE=YES 11 | shift 12 | ;; 13 | *) 14 | printf "Unknown flag: ${1}\n\n" 15 | help 16 | exit 1 17 | ;; 18 | esac 19 | done 20 | } 21 | 22 | function __main__() { 23 | export SAKE_USER_CONFIG="$PWD/user-config.yaml" 24 | parse_options $@ 25 | if [[ "$SAVE" ]]; then 26 | hyperfine -N --runs 10 '../dist/sake run ping -s server-9' > ./profiles/ping-no-key 27 | hyperfine -N --runs 10 '../dist/sake run ping --forks=1 -t reachable' > ./profiles/ping 28 | hyperfine -N --runs 10 '../dist/sake run ping --strategy=free -t reachable' > ./profiles/ping-parallel 29 | hyperfine -N --runs 10 '../dist/sake run d --forks=1 -t reachable' > ./profiles/nested 30 | hyperfine -N --runs 10 '../dist/sake run d --strategy=free -t reachable' > ./profiles/nested-parallel 31 | hyperfine -N --runs 10 '../dist/sake list servers' > ./profiles/list-servers 32 | else 33 | hyperfine -N --runs 10 '../dist/sake run ping -s server-9' 34 | hyperfine -N --runs 10 '../dist/sake run ping --forks=1 -t reachable' 35 | hyperfine -N --runs 10 '../dist/sake run ping --strategy=free -t reachable' 36 | hyperfine -N --runs 10 '../dist/sake run d --forks=1 -t reachable' 37 | hyperfine -N --runs 10 '../dist/sake run d --strategy=free -t reachable' 38 | hyperfine -N --runs 10 '../dist/sake list servers' 39 | fi 40 | } 41 | 42 | __main__ $@ 43 | -------------------------------------------------------------------------------- /test/integration/golden/golden-0.stdout: -------------------------------------------------------------------------------- 1 | Index: 0 2 | Name: List tags 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list tags 4 | WantErr: false 5 | 6 | --- 7 | 8 | tag | server 9 | -------------+------------- 10 | local | localhost 11 | reachable | localhost 12 | | list-0 13 | | list-1 14 | | range-0 15 | | range-1 16 | | inv 17 | | server-1 18 | | server-2 19 | | server-3 20 | | server-4 21 | | server-5 22 | | server-6 23 | | server-7 24 | | server-8 25 | | server-9 26 | unreachable | unreachable 27 | remote | list-0 28 | | list-1 29 | | range-0 30 | | range-1 31 | | inv 32 | | server-1 33 | | server-2 34 | | server-3 35 | | server-4 36 | | server-5 37 | | server-6 38 | | server-7 39 | | server-8 40 | | server-9 41 | | server-10 42 | | server-11 43 | prod | list-0 44 | | list-1 45 | | range-0 46 | | range-1 47 | | inv 48 | | server-1 49 | | server-2 50 | list | list-0 51 | | list-1 52 | range | range-0 53 | | range-1 54 | inv | inv 55 | demo | server-3 56 | | server-4 57 | | server-7 58 | | server-8 59 | | server-9 60 | sandbox | server-5 61 | | server-6 62 | bastion | server-10 63 | | server-11 64 | 65 | -------------------------------------------------------------------------------- /test/integration/golden/golden-1.stdout: -------------------------------------------------------------------------------- 1 | Index: 1 2 | Name: List specs 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list specs 4 | WantErr: false 5 | 6 | --- 7 | 8 | spec | describe | list_hosts | silent | hidden | strategy | batch | batch_p | forks | output | any_errors_fatal | max_fail_percentage | ignore_errors | ignore_unreachable | report | verbose | confirm | step 9 | ---------+----------+------------+--------+--------+----------+-------+---------+-------+--------+------------------+---------------------+---------------+--------------------+--------+---------+---------+------- 10 | default | false | false | false | false | linear | 1 | 0 | 10000 | table | false | 0 | false | false | recap | false | false | false 11 | table | false | false | false | false | | 0 | 0 | 10000 | table | false | 0 | false | false | recap | false | false | false 12 | text | false | false | false | false | | 0 | 0 | 10000 | text | false | 0 | false | false | recap | false | false | false 13 | info | false | false | false | false | free | 0 | 0 | 10000 | table | false | 0 | true | true | recap | false | false | false 14 | 15 | -------------------------------------------------------------------------------- /test/integration/golden/golden-10.stdout: -------------------------------------------------------------------------------- 1 | Index: 10 2 | Name: Describe servers 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers 4 | WantErr: false 5 | 6 | --- 7 | 8 | name: localhost 9 | desc: localhost 10 | user: test 11 | host: localhost 12 | port: 22 13 | local: true 14 | work_dir: /tmp 15 | tags: local, reachable 16 | 17 | -- 18 | 19 | name: unreachable 20 | user: test 21 | host: 172.24.2.50 22 | port: 22 23 | tags: unreachable 24 | 25 | -- 26 | 27 | name: list-0 28 | group: list 29 | desc: many hosts using list 30 | user: test 31 | host: 172.24.2.2 32 | port: 22 33 | tags: remote, prod, list, reachable 34 | env: 35 | hello: world 36 | 37 | -- 38 | 39 | name: list-1 40 | group: list 41 | desc: many hosts using list 42 | user: test 43 | host: 172.24.2.4 44 | port: 22 45 | tags: remote, prod, list, reachable 46 | env: 47 | hello: world 48 | 49 | -- 50 | 51 | name: range-0 52 | group: range 53 | desc: many hosts using range 54 | user: test 55 | host: 172.24.2.2 56 | port: 22 57 | tags: remote, prod, range, reachable 58 | env: 59 | hello: world 60 | 61 | -- 62 | 63 | name: range-1 64 | group: range 65 | desc: many hosts using range 66 | user: test 67 | host: 172.24.2.4 68 | port: 22 69 | tags: remote, prod, range, reachable 70 | env: 71 | hello: world 72 | 73 | -- 74 | 75 | name: inv-0 76 | group: inv 77 | desc: many hosts using inventory 78 | user: test 79 | host: 172.24.2.2 80 | port: 22 81 | tags: remote, prod, inv, reachable 82 | env: 83 | hello: world 84 | host: 172.24.2.4 85 | 86 | -- 87 | 88 | name: inv-1 89 | group: inv 90 | desc: many hosts using inventory 91 | user: test 92 | host: 172.24.2.4 93 | port: 22 94 | tags: remote, prod, inv, reachable 95 | env: 96 | hello: world 97 | host: 172.24.2.4 98 | 99 | -- 100 | 101 | name: server-1 102 | desc: server-1 103 | user: test 104 | host: 172.24.2.2 105 | port: 22 106 | work_dir: /home/test 107 | tags: remote, prod, reachable 108 | env: 109 | host: 172.24.2.2 110 | 111 | -- 112 | 113 | name: server-2 114 | desc: server-2 115 | user: test 116 | host: 172.24.2.3 117 | port: 33 118 | tags: remote, prod, reachable 119 | 120 | -- 121 | 122 | name: server-3 123 | desc: server-3 124 | user: test 125 | host: 172.24.2.4 126 | port: 22 127 | tags: remote, demo, reachable 128 | 129 | -- 130 | 131 | name: server-4 132 | desc: server-4 133 | user: test 134 | host: 172.24.2.5 135 | port: 22 136 | tags: remote, demo, reachable 137 | 138 | -- 139 | 140 | name: server-5 141 | desc: server-5 142 | user: test 143 | host: 172.24.2.6 144 | port: 22 145 | tags: remote, sandbox, reachable 146 | 147 | -- 148 | 149 | name: server-6 150 | desc: server-6 151 | user: test 152 | host: 172.24.2.7 153 | port: 22 154 | tags: remote, sandbox, reachable 155 | 156 | -- 157 | 158 | name: server-7 159 | desc: server-7 160 | user: test 161 | host: 172.24.2.8 162 | port: 22 163 | tags: remote, demo, reachable 164 | 165 | -- 166 | 167 | name: server-8 168 | desc: server-8 169 | user: test 170 | host: 172.24.2.9 171 | port: 22 172 | tags: remote, demo, reachable 173 | 174 | -- 175 | 176 | name: server-9 177 | desc: server-9 178 | user: test 179 | host: 2001:3984:3989::10 180 | port: 22 181 | tags: remote, demo, reachable 182 | 183 | -- 184 | 185 | name: server-10 186 | desc: server-10 desc 187 | user: test 188 | host: 172.24.2.10 189 | port: 22 190 | bastion: test@172.24.2.98:22 191 | work_dir: /tmp 192 | tags: remote, bastion 193 | 194 | -- 195 | 196 | name: server-11 197 | desc: server-11 desc 198 | user: test 199 | host: 172.24.2.11 200 | port: 22 201 | bastions: 202 | - test@172.24.2.98:22 203 | - test@172.24.2.99:22 204 | work_dir: /tmp 205 | tags: remote, bastion 206 | 207 | -------------------------------------------------------------------------------- /test/integration/golden/golden-11.stdout: -------------------------------------------------------------------------------- 1 | Index: 11 2 | Name: Describe servers filter on list hosts 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers list 4 | WantErr: false 5 | 6 | --- 7 | 8 | name: list-0 9 | group: list 10 | desc: many hosts using list 11 | user: test 12 | host: 172.24.2.2 13 | port: 22 14 | tags: remote, prod, list, reachable 15 | env: 16 | hello: world 17 | 18 | -- 19 | 20 | name: list-1 21 | group: list 22 | desc: many hosts using list 23 | user: test 24 | host: 172.24.2.4 25 | port: 22 26 | tags: remote, prod, list, reachable 27 | env: 28 | hello: world 29 | 30 | -------------------------------------------------------------------------------- /test/integration/golden/golden-12.stdout: -------------------------------------------------------------------------------- 1 | Index: 12 2 | Name: Describe servers filter on range hosts 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers range 4 | WantErr: false 5 | 6 | --- 7 | 8 | name: range-0 9 | group: range 10 | desc: many hosts using range 11 | user: test 12 | host: 172.24.2.2 13 | port: 22 14 | tags: remote, prod, range, reachable 15 | env: 16 | hello: world 17 | 18 | -- 19 | 20 | name: range-1 21 | group: range 22 | desc: many hosts using range 23 | user: test 24 | host: 172.24.2.4 25 | port: 22 26 | tags: remote, prod, range, reachable 27 | env: 28 | hello: world 29 | 30 | -------------------------------------------------------------------------------- /test/integration/golden/golden-13.stdout: -------------------------------------------------------------------------------- 1 | Index: 13 2 | Name: Describe servers filter on inventory hosts 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers inv 4 | WantErr: false 5 | 6 | --- 7 | 8 | name: inv-0 9 | group: inv 10 | desc: many hosts using inventory 11 | user: test 12 | host: 172.24.2.2 13 | port: 22 14 | tags: remote, prod, inv, reachable 15 | env: 16 | hello: world 17 | host: 172.24.2.4 18 | 19 | -- 20 | 21 | name: inv-1 22 | group: inv 23 | desc: many hosts using inventory 24 | user: test 25 | host: 172.24.2.4 26 | port: 22 27 | tags: remote, prod, inv, reachable 28 | env: 29 | hello: world 30 | host: 172.24.2.4 31 | 32 | -------------------------------------------------------------------------------- /test/integration/golden/golden-15.stdout: -------------------------------------------------------------------------------- 1 | Index: 15 2 | Name: Ping all servers 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | ping 11 | --------------------+------ 12 | localhost | pong 13 | 172.24.2.2 | pong 14 | 172.24.2.4 | pong 15 | 172.24.2.2 | pong 16 | 172.24.2.4 | pong 17 | 172.24.2.2 | pong 18 | 172.24.2.4 | pong 19 | 172.24.2.2 | pong 20 | 172.24.2.3 | pong 21 | 172.24.2.4 | pong 22 | 172.24.2.5 | pong 23 | 172.24.2.6 | pong 24 | 172.24.2.7 | pong 25 | 172.24.2.8 | pong 26 | 172.24.2.9 | pong 27 | 2001:3984:3989::10 | pong 28 | 29 | localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 30 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 31 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 32 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 33 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 34 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 35 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 36 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 37 | 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 38 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 39 | 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 43 | 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 44 | 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 45 | ------------------------------------------------------------------------------ 46 | Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 47 | 48 | -------------------------------------------------------------------------------- /test/integration/golden/golden-16.stdout: -------------------------------------------------------------------------------- 1 | Index: 16 2 | Name: Multiple commands 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -t prod 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | OS | Kernel 11 | ------------+----+-------- 12 | 172.24.2.2 | OS | kernel 13 | 172.24.2.4 | OS | kernel 14 | 172.24.2.2 | OS | kernel 15 | 172.24.2.4 | OS | kernel 16 | 172.24.2.2 | OS | kernel 17 | 172.24.2.4 | OS | kernel 18 | 172.24.2.2 | OS | kernel 19 | 172.24.2.3 | OS | kernel 20 | 21 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 22 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 23 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 24 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 25 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 26 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 27 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 28 | 172.24.2.3 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 29 | ---------------------------------------------------------------------- 30 | Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 31 | 32 | -------------------------------------------------------------------------------- /test/integration/golden/golden-17.stdout: -------------------------------------------------------------------------------- 1 | Index: 17 2 | Name: Filter by hosts server using server name 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s list-1 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | OS | Kernel 11 | ------------+----+-------- 12 | 172.24.2.4 | OS | kernel 13 | 14 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 15 | 16 | -------------------------------------------------------------------------------- /test/integration/golden/golden-18.stdout: -------------------------------------------------------------------------------- 1 | Index: 18 2 | Name: Filter by hosts server using range index 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s 'list[0]' 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | OS | Kernel 11 | ------------+----+-------- 12 | 172.24.2.2 | OS | kernel 13 | 14 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 15 | 16 | -------------------------------------------------------------------------------- /test/integration/golden/golden-19.stdout: -------------------------------------------------------------------------------- 1 | Index: 19 2 | Name: Filter by hosts server 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s 'list[0:2]' 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | OS | Kernel 11 | ------------+----+-------- 12 | 172.24.2.2 | OS | kernel 13 | 172.24.2.4 | OS | kernel 14 | 15 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 16 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 17 | --------------------------------------------------------------------- 18 | Total ok=4 unreachable=0 ignored=0 failed=0 skipped=0 19 | 20 | -------------------------------------------------------------------------------- /test/integration/golden/golden-2.stdout: -------------------------------------------------------------------------------- 1 | Index: 2 2 | Name: List targets 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list targets 4 | WantErr: false 5 | 6 | --- 7 | 8 | target | all | invert | limit | limit_p 9 | ---------+-------+--------+-------+--------- 10 | all | true | false | 0 | 0 11 | default | false | false | 0 | 0 12 | 13 | -------------------------------------------------------------------------------- /test/integration/golden/golden-20.stdout: -------------------------------------------------------------------------------- 1 | Index: 20 2 | Name: Filter by host regex 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -r '172.24.2.(2|4)' 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | OS | Kernel 11 | ------------+----+-------- 12 | 172.24.2.2 | OS | kernel 13 | 172.24.2.4 | OS | kernel 14 | 172.24.2.2 | OS | kernel 15 | 172.24.2.4 | OS | kernel 16 | 172.24.2.2 | OS | kernel 17 | 172.24.2.4 | OS | kernel 18 | 172.24.2.2 | OS | kernel 19 | 172.24.2.4 | OS | kernel 20 | 21 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 22 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 23 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 24 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 25 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 26 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 27 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 28 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 29 | ---------------------------------------------------------------------- 30 | Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 31 | 32 | -------------------------------------------------------------------------------- /test/integration/golden/golden-21.stdout: -------------------------------------------------------------------------------- 1 | Index: 21 2 | Name: Limit to 2 servers 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable -l 2 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | ping 11 | ------------+------ 12 | localhost | pong 13 | 172.24.2.2 | pong 14 | 15 | localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 16 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 17 | --------------------------------------------------------------------- 18 | Total ok=2 unreachable=0 ignored=0 failed=0 skipped=0 19 | 20 | -------------------------------------------------------------------------------- /test/integration/golden/golden-22.stdout: -------------------------------------------------------------------------------- 1 | Index: 22 2 | Name: Limit to 50 percent servers 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable -L 50 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | ping 11 | ------------+------ 12 | localhost | pong 13 | 172.24.2.2 | pong 14 | 172.24.2.4 | pong 15 | 172.24.2.2 | pong 16 | 172.24.2.4 | pong 17 | 172.24.2.2 | pong 18 | 172.24.2.4 | pong 19 | 172.24.2.2 | pong 20 | 21 | localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 22 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 23 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 24 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 25 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 26 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 27 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 28 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 29 | --------------------------------------------------------------------- 30 | Total ok=8 unreachable=0 ignored=0 failed=0 skipped=0 31 | 32 | -------------------------------------------------------------------------------- /test/integration/golden/golden-23.stdout: -------------------------------------------------------------------------------- 1 | Index: 23 2 | Name: Filter by inverting on tag unreachable 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t unreachable -v 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | ping 11 | --------------------+------ 12 | localhost | pong 13 | 172.24.2.2 | pong 14 | 172.24.2.4 | pong 15 | 172.24.2.2 | pong 16 | 172.24.2.4 | pong 17 | 172.24.2.2 | pong 18 | 172.24.2.4 | pong 19 | 172.24.2.2 | pong 20 | 172.24.2.3 | pong 21 | 172.24.2.4 | pong 22 | 172.24.2.5 | pong 23 | 172.24.2.6 | pong 24 | 172.24.2.7 | pong 25 | 172.24.2.8 | pong 26 | 172.24.2.9 | pong 27 | 2001:3984:3989::10 | pong 28 | 172.24.2.10 | pong 29 | 172.24.2.11 | pong 30 | 31 | localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 32 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 33 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 34 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 35 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 36 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 37 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 38 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 39 | 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 43 | 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 44 | 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 45 | 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 46 | 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 47 | 172.24.2.10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 48 | 172.24.2.11 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 49 | ------------------------------------------------------------------------------ 50 | Total ok=18 unreachable=0 ignored=0 failed=0 skipped=0 51 | 52 | -------------------------------------------------------------------------------- /test/integration/golden/golden-27.stdout: -------------------------------------------------------------------------------- 1 | Index: 27 2 | Name: Nested tasks 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run d -q -t reachable 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | ping | ping | ping | ping | ping | ping 11 | --------------------+------+------+------+------+------+------ 12 | localhost | pong | pong | pong | pong | pong | pong 13 | 172.24.2.2 | pong | pong | pong | pong | pong | pong 14 | 172.24.2.4 | pong | pong | pong | pong | pong | pong 15 | 172.24.2.2 | pong | pong | pong | pong | pong | pong 16 | 172.24.2.4 | pong | pong | pong | pong | pong | pong 17 | 172.24.2.2 | pong | pong | pong | pong | pong | pong 18 | 172.24.2.4 | pong | pong | pong | pong | pong | pong 19 | 172.24.2.2 | pong | pong | pong | pong | pong | pong 20 | 172.24.2.3 | pong | pong | pong | pong | pong | pong 21 | 172.24.2.4 | pong | pong | pong | pong | pong | pong 22 | 172.24.2.5 | pong | pong | pong | pong | pong | pong 23 | 172.24.2.6 | pong | pong | pong | pong | pong | pong 24 | 172.24.2.7 | pong | pong | pong | pong | pong | pong 25 | 172.24.2.8 | pong | pong | pong | pong | pong | pong 26 | 172.24.2.9 | pong | pong | pong | pong | pong | pong 27 | 2001:3984:3989::10 | pong | pong | pong | pong | pong | pong 28 | 29 | localhost ok=6 unreachable=0 ignored=0 failed=0 skipped=0 30 | 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 31 | 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 32 | 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 33 | 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 34 | 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 35 | 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 36 | 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 37 | 172.24.2.3 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 38 | 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 39 | 172.24.2.5 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.6 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.7 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.8 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 43 | 172.24.2.9 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 44 | 2001:3984:3989::10 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 45 | ------------------------------------------------------------------------------ 46 | Total ok=96 unreachable=0 ignored=0 failed=0 skipped=0 47 | 48 | -------------------------------------------------------------------------------- /test/integration/golden/golden-28.stdout: -------------------------------------------------------------------------------- 1 | Index: 28 2 | Name: Work Dir 1 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-1 -q -t reachable 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | ref | Override inline ref | Inline | Override inline 11 | --------------------+-------+---------------------+--------+----------------- 12 | localhost | /home | /opt | /home | / 13 | 172.24.2.2 | /home | /opt | /home | / 14 | 172.24.2.4 | /home | /opt | /home | / 15 | 172.24.2.2 | /home | /opt | /home | / 16 | 172.24.2.4 | /home | /opt | /home | / 17 | 172.24.2.2 | /home | /opt | /home | / 18 | 172.24.2.4 | /home | /opt | /home | / 19 | 172.24.2.2 | /home | /opt | /home | / 20 | 172.24.2.3 | /home | /opt | /home | / 21 | 172.24.2.4 | /home | /opt | /home | / 22 | 172.24.2.5 | /home | /opt | /home | / 23 | 172.24.2.6 | /home | /opt | /home | / 24 | 172.24.2.7 | /home | /opt | /home | / 25 | 172.24.2.8 | /home | /opt | /home | / 26 | 172.24.2.9 | /home | /opt | /home | / 27 | 2001:3984:3989::10 | /home | /opt | /home | / 28 | 29 | localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 30 | 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 31 | 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 32 | 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 33 | 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 34 | 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 35 | 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 36 | 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 37 | 172.24.2.3 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 38 | 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 39 | 172.24.2.5 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.6 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 43 | 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 44 | 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 45 | ------------------------------------------------------------------------------ 46 | Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 47 | 48 | -------------------------------------------------------------------------------- /test/integration/golden/golden-29.stdout: -------------------------------------------------------------------------------- 1 | Index: 29 2 | Name: Work Dir 2 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-2 -q -t reachable 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | ref | Override inline ref | Inline | Override inline 11 | --------------------+------+---------------------+------------+----------------- 12 | localhost | /usr | /opt | /tmp | / 13 | 172.24.2.2 | /usr | /opt | /home/test | / 14 | 172.24.2.4 | /usr | /opt | /home/test | / 15 | 172.24.2.2 | /usr | /opt | /home/test | / 16 | 172.24.2.4 | /usr | /opt | /home/test | / 17 | 172.24.2.2 | /usr | /opt | /home/test | / 18 | 172.24.2.4 | /usr | /opt | /home/test | / 19 | 172.24.2.2 | /usr | /opt | /home/test | / 20 | 172.24.2.3 | /usr | /opt | /home/test | / 21 | 172.24.2.4 | /usr | /opt | /home/test | / 22 | 172.24.2.5 | /usr | /opt | /home/test | / 23 | 172.24.2.6 | /usr | /opt | /home/test | / 24 | 172.24.2.7 | /usr | /opt | /home/test | / 25 | 172.24.2.8 | /usr | /opt | /home/test | / 26 | 172.24.2.9 | /usr | /opt | /home/test | / 27 | 2001:3984:3989::10 | /usr | /opt | /home/test | / 28 | 29 | localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 30 | 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 31 | 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 32 | 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 33 | 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 34 | 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 35 | 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 36 | 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 37 | 172.24.2.3 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 38 | 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 39 | 172.24.2.5 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.6 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 43 | 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 44 | 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 45 | ------------------------------------------------------------------------------ 46 | Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 47 | 48 | -------------------------------------------------------------------------------- /test/integration/golden/golden-3.stdout: -------------------------------------------------------------------------------- 1 | Index: 3 2 | Name: List tasks 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list tasks 4 | WantErr: false 5 | 6 | --- 7 | 8 | task | desc 9 | ------------------+---------------------- 10 | ping | ping server 11 | real-ping | ping server 12 | Host | print host 13 | Hostname | print hostname 14 | OS | print OS 15 | Kernel | Print kernel version 16 | info | get remote info 17 | env | 18 | env-ref | 19 | env-complex | 20 | env-default | 21 | a | 22 | b | 23 | c | 24 | d | 25 | ref | 26 | nested | 27 | work-dir-1 | 28 | work-dir-2 | 29 | work-dir-3 | 30 | register-1 | 31 | register-2 | 32 | fatal | 33 | fatal-true | 34 | errors | 35 | errors-true | 36 | unreachable | 37 | unreachable-true | 38 | empty | 39 | empty-true | 40 | output | 41 | 42 | -------------------------------------------------------------------------------- /test/integration/golden/golden-30.stdout: -------------------------------------------------------------------------------- 1 | Index: 30 2 | Name: Work Dir 3 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-3 -q -t reachable 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | ref | ref 11 | --------------------+------+------ 12 | localhost | /usr | /etc 13 | 172.24.2.2 | /usr | /etc 14 | 172.24.2.4 | /usr | /etc 15 | 172.24.2.2 | /usr | /etc 16 | 172.24.2.4 | /usr | /etc 17 | 172.24.2.2 | /usr | /etc 18 | 172.24.2.4 | /usr | /etc 19 | 172.24.2.2 | /usr | /etc 20 | 172.24.2.3 | /usr | /etc 21 | 172.24.2.4 | /usr | /etc 22 | 172.24.2.5 | /usr | /etc 23 | 172.24.2.6 | /usr | /etc 24 | 172.24.2.7 | /usr | /etc 25 | 172.24.2.8 | /usr | /etc 26 | 172.24.2.9 | /usr | /etc 27 | 2001:3984:3989::10 | /usr | /etc 28 | 29 | localhost ok=2 unreachable=0 ignored=0 failed=0 skipped=0 30 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 31 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 32 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 33 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 34 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 35 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 36 | 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 37 | 172.24.2.3 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 38 | 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 39 | 172.24.2.5 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.6 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.7 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.8 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 43 | 172.24.2.9 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 44 | 2001:3984:3989::10 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 45 | ------------------------------------------------------------------------------ 46 | Total ok=32 unreachable=0 ignored=0 failed=0 skipped=0 47 | 48 | -------------------------------------------------------------------------------- /test/integration/golden/golden-31.stdout: -------------------------------------------------------------------------------- 1 | Index: 31 2 | Name: Register 1 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run register-1 -q -t reachable 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | task-0 11 | --------------------+-------- 12 | localhost | foo 13 | 172.24.2.2 | foo 14 | 172.24.2.4 | foo 15 | 172.24.2.2 | foo 16 | 172.24.2.4 | foo 17 | 172.24.2.2 | foo 18 | 172.24.2.4 | foo 19 | 172.24.2.2 | foo 20 | 172.24.2.3 | foo 21 | 172.24.2.4 | foo 22 | 172.24.2.5 | foo 23 | 172.24.2.6 | foo 24 | 172.24.2.7 | foo 25 | 172.24.2.8 | foo 26 | 172.24.2.9 | foo 27 | 2001:3984:3989::10 | foo 28 | 29 | localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 30 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 31 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 32 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 33 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 34 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 35 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 36 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 37 | 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 38 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 39 | 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 43 | 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 44 | 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 45 | ------------------------------------------------------------------------------ 46 | Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 47 | 48 | -------------------------------------------------------------------------------- /test/integration/golden/golden-33.stdout: -------------------------------------------------------------------------------- 1 | Index: 33 2 | Name: fatal false 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run fatal -q -t reachable 4 | WantErr: true 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | fatal 11 | --------------------+------------------------------ 12 | localhost | 13 | | exit status 1 14 | 172.24.2.2 | 15 | | Process exited with status 1 16 | 172.24.2.4 | 17 | | Process exited with status 1 18 | 172.24.2.2 | 19 | | Process exited with status 1 20 | 172.24.2.4 | 21 | | Process exited with status 1 22 | 172.24.2.2 | 23 | | Process exited with status 1 24 | 172.24.2.4 | 25 | | Process exited with status 1 26 | 172.24.2.2 | 27 | | Process exited with status 1 28 | 172.24.2.3 | 29 | | Process exited with status 1 30 | 172.24.2.4 | 31 | | Process exited with status 1 32 | 172.24.2.5 | 33 | | Process exited with status 1 34 | 172.24.2.6 | 35 | | Process exited with status 1 36 | 172.24.2.7 | 37 | | Process exited with status 1 38 | 172.24.2.8 | 39 | | Process exited with status 1 40 | 172.24.2.9 | 41 | | Process exited with status 1 42 | 2001:3984:3989::10 | 43 | | Process exited with status 1 44 | 45 | localhost ok=0 unreachable=0 ignored=0 failed=1 skipped=0 46 | 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 47 | 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 48 | 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 49 | 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 50 | 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 51 | 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 52 | 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 53 | 172.24.2.3 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 54 | 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 55 | 172.24.2.5 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 56 | 172.24.2.6 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 57 | 172.24.2.7 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 58 | 172.24.2.8 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 59 | 172.24.2.9 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 60 | 2001:3984:3989::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 61 | ------------------------------------------------------------------------------ 62 | Total ok=0 unreachable=0 ignored=0 failed=16 skipped=0 63 | 64 | exit status 1 65 | -------------------------------------------------------------------------------- /test/integration/golden/golden-34.stdout: -------------------------------------------------------------------------------- 1 | Index: 34 2 | Name: fatal true 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run fatal-true -q -t reachable 4 | WantErr: true 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | fatal-true 11 | --------------------+------------------------------ 12 | localhost | 13 | | exit status 1 14 | 172.24.2.2 | 15 | | Process exited with status 1 16 | 172.24.2.4 | 17 | | Process exited with status 1 18 | 172.24.2.2 | 19 | | Process exited with status 1 20 | 172.24.2.4 | 21 | | Process exited with status 1 22 | 172.24.2.2 | 23 | | Process exited with status 1 24 | 172.24.2.4 | 25 | | Process exited with status 1 26 | 172.24.2.2 | 27 | | Process exited with status 1 28 | 172.24.2.3 | 29 | | Process exited with status 1 30 | 172.24.2.4 | 31 | | Process exited with status 1 32 | 172.24.2.5 | 33 | | Process exited with status 1 34 | 172.24.2.6 | 35 | | Process exited with status 1 36 | 172.24.2.7 | 37 | | Process exited with status 1 38 | 172.24.2.8 | 39 | | Process exited with status 1 40 | 172.24.2.9 | 41 | | Process exited with status 1 42 | 2001:3984:3989::10 | 43 | | Process exited with status 1 44 | 45 | localhost ok=0 unreachable=0 ignored=0 failed=1 skipped=0 46 | 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 47 | 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 48 | 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 49 | 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 50 | 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 51 | 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 52 | 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 53 | 172.24.2.3 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 54 | 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 55 | 172.24.2.5 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 56 | 172.24.2.6 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 57 | 172.24.2.7 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 58 | 172.24.2.8 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 59 | 172.24.2.9 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 60 | 2001:3984:3989::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 61 | ------------------------------------------------------------------------------ 62 | Total ok=0 unreachable=0 ignored=0 failed=16 skipped=0 63 | 64 | exit status 1 65 | -------------------------------------------------------------------------------- /test/integration/golden/golden-37.stdout: -------------------------------------------------------------------------------- 1 | Index: 37 2 | Name: unreachable false 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run unreachable -q -a 4 | WantErr: true 5 | 6 | --- 7 | 8 | 9 | Unreachable Hosts 10 | 11 | server | host | user | port | error 12 | -------------+-------------+------+------+---------------------------------------------------- 13 | unreachable | 172.24.2.50 | test | 22 | dial tcp 172.24.2.50:22: connect: no route to host 14 | 15 | exit status 4 16 | -------------------------------------------------------------------------------- /test/integration/golden/golden-38.stdout: -------------------------------------------------------------------------------- 1 | Index: 38 2 | Name: unreachable true 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run unreachable-true -o table -q -a 4 | WantErr: false 5 | 6 | --- 7 | 8 | 9 | Unreachable Hosts 10 | 11 | server | host | user | port | error 12 | -------------+-------------+------+------+---------------------------------------------------- 13 | unreachable | 172.24.2.50 | test | 22 | dial tcp 172.24.2.50:22: connect: no route to host 14 | 15 | 16 | TASKS 17 | 18 | host | unreachable-true 19 | --------------------+------------------ 20 | localhost | 123 21 | 172.24.2.2 | 123 22 | 172.24.2.4 | 123 23 | 172.24.2.2 | 123 24 | 172.24.2.4 | 123 25 | 172.24.2.2 | 123 26 | 172.24.2.4 | 123 27 | 172.24.2.2 | 123 28 | 172.24.2.3 | 123 29 | 172.24.2.4 | 123 30 | 172.24.2.5 | 123 31 | 172.24.2.6 | 123 32 | 172.24.2.7 | 123 33 | 172.24.2.8 | 123 34 | 172.24.2.9 | 123 35 | 2001:3984:3989::10 | 123 36 | 172.24.2.10 | 123 37 | 172.24.2.11 | 123 38 | 39 | localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 43 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 44 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 45 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 46 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 47 | 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 48 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 49 | 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 50 | 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 51 | 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 52 | 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 53 | 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 54 | 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 55 | 172.24.2.10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 56 | 172.24.2.11 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 57 | 172.24.2.50 ok=0 unreachable=1 ignored=0 failed=0 skipped=0 58 | ------------------------------------------------------------------------------ 59 | Total ok=18 unreachable=1 ignored=0 failed=0 skipped=0 60 | 61 | -------------------------------------------------------------------------------- /test/integration/golden/golden-39.stdout: -------------------------------------------------------------------------------- 1 | Index: 39 2 | Name: omit_empty false 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run empty -q -t reachable 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | empty 11 | --------------------+-------- 12 | localhost | 13 | 172.24.2.2 | Exists 14 | 172.24.2.4 | Exists 15 | 172.24.2.2 | Exists 16 | 172.24.2.4 | Exists 17 | 172.24.2.2 | Exists 18 | 172.24.2.4 | Exists 19 | 172.24.2.2 | Exists 20 | 172.24.2.3 | Exists 21 | 172.24.2.4 | Exists 22 | 172.24.2.5 | Exists 23 | 172.24.2.6 | Exists 24 | 172.24.2.7 | Exists 25 | 172.24.2.8 | Exists 26 | 172.24.2.9 | Exists 27 | 2001:3984:3989::10 | Exists 28 | 29 | localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 30 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 31 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 32 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 33 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 34 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 35 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 36 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 37 | 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 38 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 39 | 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 43 | 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 44 | 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 45 | ------------------------------------------------------------------------------ 46 | Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 47 | 48 | -------------------------------------------------------------------------------- /test/integration/golden/golden-4.stdout: -------------------------------------------------------------------------------- 1 | Index: 4 2 | Name: List servers 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers 4 | WantErr: false 5 | 6 | --- 7 | 8 | server | host | tags | desc 9 | -------------+--------------------+-----------------------------+---------------------------- 10 | localhost | localhost | local,reachable | localhost 11 | unreachable | 172.24.2.50 | unreachable | 12 | list-0 | 172.24.2.2 | remote,prod,list,reachable | many hosts using list 13 | list-1 | 172.24.2.4 | remote,prod,list,reachable | many hosts using list 14 | range-0 | 172.24.2.2 | remote,prod,range,reachable | many hosts using range 15 | range-1 | 172.24.2.4 | remote,prod,range,reachable | many hosts using range 16 | inv-0 | 172.24.2.2 | remote,prod,inv,reachable | many hosts using inventory 17 | inv-1 | 172.24.2.4 | remote,prod,inv,reachable | many hosts using inventory 18 | server-1 | 172.24.2.2 | remote,prod,reachable | server-1 19 | server-2 | 172.24.2.3 | remote,prod,reachable | server-2 20 | server-3 | 172.24.2.4 | remote,demo,reachable | server-3 21 | server-4 | 172.24.2.5 | remote,demo,reachable | server-4 22 | server-5 | 172.24.2.6 | remote,sandbox,reachable | server-5 23 | server-6 | 172.24.2.7 | remote,sandbox,reachable | server-6 24 | server-7 | 172.24.2.8 | remote,demo,reachable | server-7 25 | server-8 | 172.24.2.9 | remote,demo,reachable | server-8 26 | server-9 | 2001:3984:3989::10 | remote,demo,reachable | server-9 27 | server-10 | 172.24.2.10 | remote,bastion | server-10 desc 28 | server-11 | 172.24.2.11 | remote,bastion | server-11 desc 29 | 30 | -------------------------------------------------------------------------------- /test/integration/golden/golden-40.stdout: -------------------------------------------------------------------------------- 1 | Index: 40 2 | Name: omit_empty true 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run empty-true -q -t reachable 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | empty-true 11 | --------------------+------------ 12 | 172.24.2.2 | Exists 13 | 172.24.2.4 | Exists 14 | 172.24.2.2 | Exists 15 | 172.24.2.4 | Exists 16 | 172.24.2.2 | Exists 17 | 172.24.2.4 | Exists 18 | 172.24.2.2 | Exists 19 | 172.24.2.3 | Exists 20 | 172.24.2.4 | Exists 21 | 172.24.2.5 | Exists 22 | 172.24.2.6 | Exists 23 | 172.24.2.7 | Exists 24 | 172.24.2.8 | Exists 25 | 172.24.2.9 | Exists 26 | 2001:3984:3989::10 | Exists 27 | 28 | localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 29 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 30 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 31 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 32 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 33 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 34 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 35 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 36 | 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 37 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 38 | 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 39 | 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 43 | 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 44 | ------------------------------------------------------------------------------ 45 | Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 46 | 47 | -------------------------------------------------------------------------------- /test/integration/golden/golden-41.stdout: -------------------------------------------------------------------------------- 1 | Index: 41 2 | Name: output 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run output -q -t reachable 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | task-0 | task-1 | task-2 11 | --------------------+-------------+-----------+------------------- 12 | localhost | Hello world | Bye world | Hello again world 13 | 172.24.2.2 | Hello world | Bye world | Hello again world 14 | 172.24.2.4 | Hello world | Bye world | Hello again world 15 | 172.24.2.2 | Hello world | Bye world | Hello again world 16 | 172.24.2.4 | Hello world | Bye world | Hello again world 17 | 172.24.2.2 | Hello world | Bye world | Hello again world 18 | 172.24.2.4 | Hello world | Bye world | Hello again world 19 | 172.24.2.2 | Hello world | Bye world | Hello again world 20 | 172.24.2.3 | Hello world | Bye world | Hello again world 21 | 172.24.2.4 | Hello world | Bye world | Hello again world 22 | 172.24.2.5 | Hello world | Bye world | Hello again world 23 | 172.24.2.6 | Hello world | Bye world | Hello again world 24 | 172.24.2.7 | Hello world | Bye world | Hello again world 25 | 172.24.2.8 | Hello world | Bye world | Hello again world 26 | 172.24.2.9 | Hello world | Bye world | Hello again world 27 | 2001:3984:3989::10 | Hello world | Bye world | Hello again world 28 | 29 | localhost ok=3 unreachable=0 ignored=0 failed=0 skipped=0 30 | 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 31 | 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 32 | 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 33 | 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 34 | 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 35 | 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 36 | 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 37 | 172.24.2.3 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 38 | 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 39 | 172.24.2.5 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.6 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.7 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.8 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 43 | 172.24.2.9 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 44 | 2001:3984:3989::10 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 45 | ------------------------------------------------------------------------------ 46 | Total ok=48 unreachable=0 ignored=0 failed=0 skipped=0 47 | 48 | -------------------------------------------------------------------------------- /test/integration/golden/golden-42.stdout: -------------------------------------------------------------------------------- 1 | Index: 42 2 | Name: Run exec command 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go exec 'echo 123' -q -t reachable 4 | WantErr: false 5 | 6 | --- 7 | 8 | TASKS 9 | 10 | host | task-0 11 | --------------------+-------- 12 | localhost | 123 13 | 172.24.2.2 | 123 14 | 172.24.2.4 | 123 15 | 172.24.2.2 | 123 16 | 172.24.2.4 | 123 17 | 172.24.2.2 | 123 18 | 172.24.2.4 | 123 19 | 172.24.2.2 | 123 20 | 172.24.2.3 | 123 21 | 172.24.2.4 | 123 22 | 172.24.2.5 | 123 23 | 172.24.2.6 | 123 24 | 172.24.2.7 | 123 25 | 172.24.2.8 | 123 26 | 172.24.2.9 | 123 27 | 2001:3984:3989::10 | 123 28 | 29 | localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 30 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 31 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 32 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 33 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 34 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 35 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 36 | 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 37 | 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 38 | 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 39 | 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 40 | 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 41 | 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 42 | 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 43 | 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 44 | 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 45 | ------------------------------------------------------------------------------ 46 | Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 47 | 48 | -------------------------------------------------------------------------------- /test/integration/golden/golden-5.stdout: -------------------------------------------------------------------------------- 1 | Index: 5 2 | Name: List servers filter on list hosts 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers list 4 | WantErr: false 5 | 6 | --- 7 | 8 | server | host | tags | desc 9 | --------+------------+----------------------------+----------------------- 10 | list-0 | 172.24.2.2 | remote,prod,list,reachable | many hosts using list 11 | list-1 | 172.24.2.4 | remote,prod,list,reachable | many hosts using list 12 | 13 | -------------------------------------------------------------------------------- /test/integration/golden/golden-6.stdout: -------------------------------------------------------------------------------- 1 | Index: 6 2 | Name: List servers filter on range hosts 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers range 4 | WantErr: false 5 | 6 | --- 7 | 8 | server | host | tags | desc 9 | ---------+------------+-----------------------------+------------------------ 10 | range-0 | 172.24.2.2 | remote,prod,range,reachable | many hosts using range 11 | range-1 | 172.24.2.4 | remote,prod,range,reachable | many hosts using range 12 | 13 | -------------------------------------------------------------------------------- /test/integration/golden/golden-7.stdout: -------------------------------------------------------------------------------- 1 | Index: 7 2 | Name: List servers filter on inventory hosts 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers inv 4 | WantErr: false 5 | 6 | --- 7 | 8 | server | host | tags | desc 9 | --------+------------+---------------------------+---------------------------- 10 | inv-0 | 172.24.2.2 | remote,prod,inv,reachable | many hosts using inventory 11 | inv-1 | 172.24.2.4 | remote,prod,inv,reachable | many hosts using inventory 12 | 13 | -------------------------------------------------------------------------------- /test/integration/golden/golden-8.stdout: -------------------------------------------------------------------------------- 1 | Index: 8 2 | Name: Describe specs 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe specs 4 | WantErr: false 5 | 6 | --- 7 | name: default 8 | strategy: linear 9 | batch: 1 10 | forks: 10000 11 | output: table 12 | report: recap 13 | 14 | -- 15 | 16 | name: table 17 | forks: 10000 18 | output: table 19 | report: recap 20 | 21 | -- 22 | 23 | name: text 24 | forks: 10000 25 | output: text 26 | report: recap 27 | 28 | -- 29 | 30 | name: info 31 | strategy: free 32 | forks: 10000 33 | output: table 34 | ignore_errors: true 35 | ignore_unreachable: true 36 | report: recap 37 | -------------------------------------------------------------------------------- /test/integration/golden/golden-9.stdout: -------------------------------------------------------------------------------- 1 | Index: 9 2 | Name: Describe targets 3 | Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe targets 4 | WantErr: false 5 | 6 | --- 7 | name: all 8 | all: true 9 | 10 | -- 11 | 12 | -------------------------------------------------------------------------------- /test/keys/id_ed25519_pem: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDtsxGYQ5 3 | RtBAM+FCN+zqceAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIPdSXo09a9pJE7GZ 4 | 2Hlj6ZtMF16kmpwrzBjWna6GFMzZAAAAoClmvUuTMLHzZbOOpxpDyD/y3a7h6BzNWDAza2 5 | v883X3C9odcoQwmHnisS2X0wY/Fei7WRdDTkGpEWpxTWFwSHy8lLcsyYcEcZ6FWzt2aJXE 6 | wrab+PAVuhYn+m/N0zrIHKWrmMW+7QQDF7BKh3Xr9dDsuEtDagazmXK5R2JIvXArEMCtq0 7 | X8sZCEslE6SMzGrKEV/2mBZRQUROd3/iRnJxw= 8 | -----END OPENSSH PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /test/keys/id_ed25519_pem.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPdSXo09a9pJE7GZ2Hlj6ZtMF16kmpwrzBjWna6GFMzZ test@test.test 2 | -------------------------------------------------------------------------------- /test/keys/id_ed25519_pem_no: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACC43NzB4YxAOVwA+oRjohvOZ/BvEqtRlFUeemoGIdFYGgAAAJhHXo+cR16P 4 | nAAAAAtzc2gtZWQyNTUxOQAAACC43NzB4YxAOVwA+oRjohvOZ/BvEqtRlFUeemoGIdFYGg 5 | AAAEBibMJKRalvjkeWGzYIUMj5adSmcJ8SvLWMh+72oTW5Zrjc3MHhjEA5XAD6hGOiG85n 6 | 8G8Sq1GUVR56agYh0VgaAAAADnRlc3RAdGVzdC50ZXN0AQIDBAUGBw== 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /test/keys/id_ed25519_pem_no.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjc3MHhjEA5XAD6hGOiG85n8G8Sq1GUVR56agYh0Vga test@test.test 2 | -------------------------------------------------------------------------------- /test/keys/id_ed25519_rfc: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABD3J0F+LB 3 | m18nyRiPaezChfAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIH9qCz2Ioueqf4ad 4 | /uzSh73PQ9NtIxxl800MI6XZMthXAAAAoANJGMOhK7LiXYqK3NBvK3nLLu95BHgtxHkjIz 5 | djtGNlTE8RrMy/RkqkUidlsiSabvBRVRlAUQWDhBgR6WYgEZLok9bFLdE1xSfLfUPnA1C7 6 | 2szDnjCIEBGX3BiEmIsA2wRVBmL13w+yT7NHss2NHy4QNHdCK0i5EgyGQYotCpDlD1y9N4 7 | HGatxnW8Cb5pxyKs/OU3fSQUKFssqna5BUgbk= 8 | -----END OPENSSH PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /test/keys/id_ed25519_rfc.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9qCz2Ioueqf4ad/uzSh73PQ9NtIxxl800MI6XZMthX test@test.test 2 | -------------------------------------------------------------------------------- /test/keys/id_ed25519_rfc_no: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACBCwWFfB3PeETIgDxaT9REqXDPnbjEapIwt/Hk+3tIyoAAAAJjxxsUk8cbF 4 | JAAAAAtzc2gtZWQyNTUxOQAAACBCwWFfB3PeETIgDxaT9REqXDPnbjEapIwt/Hk+3tIyoA 5 | AAAECpxzZdrmTYIuSyiCudh5dim9FM7YIhFn+EZbvDPT9qoELBYV8Hc94RMiAPFpP1ESpc 6 | M+duMRqkjC38eT7e0jKgAAAADnRlc3RAdGVzdC50ZXN0AQIDBAUGBw== 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /test/keys/id_ed25519_rfc_no.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIELBYV8Hc94RMiAPFpP1ESpcM+duMRqkjC38eT7e0jKg test@test.test 2 | -------------------------------------------------------------------------------- /test/keys/id_rsa_pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-128-CBC,A3382AEA98F972E7D8BCE019421DC96C 4 | 5 | 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 42 | -----END RSA PRIVATE KEY----- 43 | -------------------------------------------------------------------------------- /test/keys/id_rsa_pem.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQChS2wcCaC7PP9YsjDdw7ZYOKefER7f5ve3S0tuDWvuDNJMKt+zzyFILxLLWL9on/H4SaUN6u1REzP82/buH20t6Id+32WQwsO99dm0oANfA6EUrmMIIV5rwMj+FIJl75t73eaHKoj1Kx9zqflmtQNxtBy9rwRI7PqBFHaSskqVQT0QF8YX03VE1Mfc5Do0XgaY1v7ayR2WDO3oJhPGQLQn66ooy5dtScCHYRQdXPNDZV9QybOxPD6UZSWOmTjiQicLLxDdjhkOD6moxZHdmTTvUnPVg8o0dimHk0N7YQquoXntVWY6tdJuzdPufgtAayNlvT1vgrHj3cwEU9XtMzoXhoqcFvt0uYh/LCDtaUdgVxpknbjN9r/8d8BuKPIQ9uxP71ZqTvonZwHdr+aqaieS2oaOEAUtwflgU2ZreBKYlw4GYv1DMyul4xf//SYMfIwJOXNRelk0brNUW/WZJPbLi+25NqAmTNyxE1MjiTG3dCIKgIP63jl5Y/0EEvG9k1s= test@test.test 2 | -------------------------------------------------------------------------------- /test/keys/id_rsa_pem_no: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 39 | -----END RSA PRIVATE KEY----- 40 | -------------------------------------------------------------------------------- /test/keys/id_rsa_pem_no.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDqWyIZv7SvtgnK8YdfTeMsygYe8n6AkOQaZuGM+v6XmMFq5ALI+PCk4T5UwzpQH6+aFIQ0iCFXyJ7WhBgKurX9vP3F51Ul9zI5KBZ91PCwd8KVFRUZEBZ2w7xm1EuFqa1rxG9F9lcoYw33+c3mm6R8zmqdrXV2e9FLiTAvfTX4STYhPx1z6SOxzNz9rgGZdL3PoWgYOZfDgWeb4421I46klPbtQ0dwJB8+w6nblJbZVubkXoTYsYtcFrxR//ibKxTPxqgXNSt1Wd3ePdd38qgAnDlMUgpUGjx6wylRky97VA+6at9yWhsBwcaxvkWNgXbGpML05izg8bZMAzmcUCqIWfrQqTOqNxGCftpdbpgIpJw5QmV5kueFNvBqAcs98rW2rBVhhJWT6mLXcoxvy3WAbjYQJZrxFJd9kHU/1WVO8F2vjEZObYEOTLVCf1+xdeD1XwJB5eo6ppZi3xtT7YsZkjqhBjkqH4f2dVvUQtunY7BDYR9FbwQswZXnGnHjb5k= test@test.test 2 | -------------------------------------------------------------------------------- /test/keys/id_rsa_rfc: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | 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 38 | /e+kDpEg16uJX+wH0UqINMXsUyU= 39 | -----END OPENSSH PRIVATE KEY----- 40 | -------------------------------------------------------------------------------- /test/keys/id_rsa_rfc.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC+nW+SA41wGn0064WZ8xY7ImMyEYY0yv0ySRMvnyQcvQ179EqLxrDSpqudXBrUYfo5BOzKGgemhbHL9guSboEnEYDmSZu+EHf7CILVVcERrnPXAulVq+5BcrqfIJQ0x+NAfWWYegk7w3C02YV2FtLFJwrjiQb5at+wSQPPt+lo5Dc2pOZ/mv3k5GEe0po8Ou1gSX9zzyTLfpHMDMQIuESynuLJ7Zc9oZeWF9lDKKyyk43WtqYAWkCU4dxTD6ZGV48PgDpg39dZz7B1GQvOxzRNNGUuiKLCdOmkarF7TdcoKpTLn4bVofUFhlWuvSGffoaz/AlKbJ9sFWCRCY/hMY4NtxvMfYMSlI9xY5KGxKvgL0N0UXTayp/lL5jK0vIkxnArQNmPfZw4dEqxcITJq65s2ZaqCOyHilhoyjvcAeexU/PkpGxqdLW6gK6z5eUBA78P+j3n88o1BTb3YJUPcmCqjEZc5bmxchG4NNzueyM/9WPRP0E4jVH0FapeUEUS4jE= test@test.test 2 | -------------------------------------------------------------------------------- /test/keys/id_rsa_rfc_no: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | 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 38 | -----END OPENSSH PRIVATE KEY----- 39 | -------------------------------------------------------------------------------- /test/keys/id_rsa_rfc_no.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDUu7XdxWcTqd7h+QAQFc3atFm/qt2FBEA+PwM+m3LRwgMy2rncVcg+w7g+GNTG7mqTjrbZQb8Dq7hS5RJRwdZkW+SOI3LF2pE3eo6bhtHNx9vHf6KIDL7k/6qQgHTjsKS5AGD2Gg44tEC/nyeNmdlMkO/0kqegEBWqSVh8P3SPm/nthTSKwPR5R8v5WfiBoiEgXbKSD78L5uRO5Ieu4FPL37T2wclgL2tNVQcIbSVldYYK9lzMVdf+W/Hxca2ZW4qCl3KtUORTKu0qIo17yDSjf0hG/yxZ6AMK5pqqg3ZILo9Z0+x0lS+/FAocOXW48BFHupaheIsQfOfjoIE1E5n24ugCxzgYrL3ooxdHUkSeon+5R9TQSTcE2j3o7rBOWNu5YVtTug87kz5SiYC+BvutAiH7EF69h/+4lfUn8QtWKL3DGCNHqvqz2ITF76b8oJUPTvHoksFBDRl/+78z4DyehZ4c6bRCYMI+ZXp6oYISO0pAsJKnc+okjOOF8MEifZc= test@test.test 2 | -------------------------------------------------------------------------------- /test/playground/env/sake.yaml: -------------------------------------------------------------------------------- 1 | disable_verify_host: true 2 | 3 | servers: 4 | localhost: 5 | host: localhost 6 | local: true 7 | 8 | server-1: 9 | host: 172.24.2.2 10 | env: 11 | foo: foo 12 | hello: world 13 | release: v1.0.0 14 | 15 | pihole: 16 | host: 172.24.2.3 17 | 18 | env: 19 | foo: bar 20 | hello: adios 21 | cookie: monster 22 | 23 | tasks: 24 | env: 25 | spec: 26 | output: table 27 | target: 28 | all: true 29 | env: 30 | foo: xyz 31 | task: local 32 | cmd: | 33 | echo "foo $foo" 34 | echo "hello $hello" 35 | echo "cookie $cookie" 36 | echo "release $release" 37 | echo "task $task" 38 | 39 | env-ref: 40 | spec: 41 | output: table 42 | target: 43 | all: true 44 | env: 45 | task: 123 46 | xyz: xyz 47 | cmd: | 48 | echo "foo $foo" 49 | echo "hello $hello" 50 | echo "cookie $cookie" 51 | echo "release $release" 52 | echo "task $task" 53 | echo "xyz $xyz" 54 | 55 | env-complex: 56 | spec: 57 | output: table 58 | target: 59 | all: true 60 | env: 61 | foo: xyz 62 | task: local 63 | tasks: 64 | - task: env-ref 65 | 66 | - task: env-ref 67 | env: 68 | task: remote 69 | 70 | env-default: 71 | spec: 72 | output: table 73 | target: 74 | all: true 75 | cmd: | 76 | echo "# SERVER" 77 | echo "S_TAGS $S_TAGS" 78 | echo "S_HOST $S_HOST" 79 | echo "S_USER $S_USER" 80 | echo "S_PORT $S_PORT" 81 | 82 | echo 83 | echo "# TASK" 84 | 85 | echo 86 | echo "# CONFIG" 87 | echo "SAKE_DIR $SAKE_DIR" 88 | echo "SAKE_PATH $SAKE_PATH" 89 | echo "SAKE_KNOWN_HOSTS_FILE $SAKE_KNOWN_HOSTS_FILE" 90 | -------------------------------------------------------------------------------- /test/playground/import/imports/sake-1.yaml: -------------------------------------------------------------------------------- 1 | import: 2 | - ./sake-2.yaml 3 | 4 | servers: 5 | server-1: 6 | host: server-1.lan 7 | 8 | tasks: 9 | simple: 10 | spec: table 11 | target: all 12 | cmd: echo $foo 13 | 14 | ping: 15 | desc: kaka 16 | spec: table 17 | target: all 18 | cmd: echo $ping 19 | -------------------------------------------------------------------------------- /test/playground/import/imports/sake-2.yaml: -------------------------------------------------------------------------------- 1 | servers: 2 | pihole: 3 | host: pihole.lan 4 | 5 | specs: 6 | table: 7 | output: table 8 | 9 | targets: 10 | all: 11 | all: true 12 | 13 | env: 14 | foo: bar 15 | ping: $(echo pong) 16 | 17 | tasks: 18 | echo: 19 | spec: table 20 | target: all 21 | cmd: echo "Hello World" 22 | 23 | pwd: 24 | work_dir: ../../../ 25 | cmd: pwd 26 | 27 | script: 28 | cmd: ./script.sh 29 | -------------------------------------------------------------------------------- /test/playground/import/sake.yaml: -------------------------------------------------------------------------------- 1 | disable_verify_host: true 2 | 3 | import: 4 | - ./imports/sake-1.yaml 5 | 6 | servers: 7 | localhost: 8 | host: localhost 9 | local: true 10 | 11 | themes: 12 | default: 13 | text: 14 | prefix: true 15 | 16 | tasks: 17 | ping: 18 | desc: kaka 19 | spec: table 20 | target: all 21 | cmd: echo $ping 22 | -------------------------------------------------------------------------------- /test/playground/import/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Hello World" 4 | -------------------------------------------------------------------------------- /test/playground/inventory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "172.24.2.2" 4 | echo "$hosts" 5 | echo "$1" 6 | -------------------------------------------------------------------------------- /test/playground/list-of-servers.txt: -------------------------------------------------------------------------------- 1 | 172.24.2.2 2 | 172.24.2.4 3 | -------------------------------------------------------------------------------- /test/playground/misc/common/common.yaml: -------------------------------------------------------------------------------- 1 | tasks: 2 | # ping: 3 | # cmd: | 4 | # pong="pong" 5 | # echo $pong 6 | 7 | ping: 8 | cmd: | 9 | ls -alh 10 | echo $SHELL 11 | 12 | upload: 13 | desc: upload file or directory 14 | env: 15 | SRC: "" 16 | DEST: "" 17 | local: true 18 | cmd: | 19 | # should show /misc/roles 20 | pwd 21 | -------------------------------------------------------------------------------- /test/playground/misc/roles/hello.txt: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /test/playground/misc/roles/tasks.yaml: -------------------------------------------------------------------------------- 1 | tasks: 2 | setup-pihole: 3 | target: 4 | servers: [pihole] 5 | tasks: 6 | - task: upload-pihole-files 7 | - task: ping 8 | 9 | upload-pihole-files: 10 | env: 11 | SRC: hello.txt 12 | DEST: /home/samir/hello.txt 13 | task: upload 14 | -------------------------------------------------------------------------------- /test/playground/misc/sake.yaml: -------------------------------------------------------------------------------- 1 | disable_verify_host: true 2 | 3 | import: 4 | - ./roles/tasks.yaml 5 | - ./common/common.yaml 6 | 7 | # shell: node 8 | 9 | servers: 10 | localhost: 11 | desc: localhost 12 | host: localhost 13 | local: true 14 | tags: [local] 15 | work_dir: /opt 16 | 17 | pihole: 18 | desc: runs pihole 19 | tags: [remote, pi] 20 | host: 172.24.2.2 21 | user: test 22 | identity_file: ../../keys/id_ed25519_pem 23 | password: testing 24 | 25 | server-1: 26 | desc: hosts mealie, node-red 27 | host: 172.24.2.3 28 | tags: [remote, pi] 29 | work_dir: /home/samir 30 | -------------------------------------------------------------------------------- /test/playground/nested/sake.yaml: -------------------------------------------------------------------------------- 1 | disable_verify_host: true 2 | 3 | servers: 4 | localhost: 5 | host: localhost 6 | local: true 7 | 8 | server-1: 9 | host: server-1.lan 10 | 11 | tasks: 12 | ping: 13 | cmd: echo pong 14 | 15 | a: 16 | task: ping 17 | 18 | b: 19 | tasks: 20 | - task: a 21 | - task: a 22 | 23 | c: 24 | tasks: 25 | - task: b 26 | - task: a 27 | 28 | d: 29 | spec: 30 | output: table 31 | target: 32 | all: true 33 | tasks: 34 | - task: c 35 | - task: c 36 | 37 | -------------------------------------------------------------------------------- /test/playground/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Hello World" 4 | 5 | echo $0 $1 $2 6 | -------------------------------------------------------------------------------- /test/playground/spec/sake.yaml: -------------------------------------------------------------------------------- 1 | disable_verify_host: true 2 | 3 | servers: 4 | localhost: 5 | host: localhost 6 | local: true 7 | tags: [reachable] 8 | 9 | server-1: 10 | host: server-1.lan 11 | tags: [reachable] 12 | 13 | unreachable: 14 | host: unreachable.lan 15 | 16 | tasks: 17 | ping: 18 | cmd: echo pong 19 | 20 | fatal: 21 | spec: 22 | output: table 23 | any_errors_fatal: false 24 | target: 25 | tags: [reachable] 26 | cmd: exit 1 27 | 28 | fatal-true: 29 | spec: 30 | output: table 31 | any_errors_fatal: true 32 | target: 33 | tags: [reachable] 34 | cmd: exit 1 35 | 36 | errors: 37 | spec: 38 | output: table 39 | ignore_errors: false 40 | target: 41 | tags: [reachable] 42 | tasks: 43 | - cmd: echo 123 44 | - cmd: exit 321 45 | - cmd: echo 321 46 | 47 | errors-true: 48 | spec: 49 | output: table 50 | ignore_errors: true 51 | target: 52 | tags: [reachable] 53 | tasks: 54 | - cmd: echo 123 55 | - cmd: exit 321 56 | - cmd: echo 321 57 | 58 | unreachable: 59 | spec: 60 | ignore_unreachable: false 61 | target: 62 | all: true 63 | cmd: echo 123 64 | 65 | unreachable-true: 66 | spec: 67 | ignore_unreachable: true 68 | target: 69 | all: true 70 | cmd: echo 123 71 | 72 | empty: 73 | spec: 74 | omit_empty: false 75 | output: table 76 | target: 77 | tags: [reachable] 78 | cmd: | 79 | if [[ -d ".ssh" ]] 80 | then 81 | echo "Exists" 82 | fi 83 | 84 | empty-true: 85 | spec: 86 | omit_empty: true 87 | output: table 88 | target: 89 | tags: [reachable] 90 | cmd: | 91 | if [[ -d ".ssh" ]] 92 | then 93 | echo "Exists" 94 | fi 95 | 96 | output: 97 | spec: 98 | output: text 99 | tasks: 100 | - cmd: echo "Hello world" 101 | - cmd: echo "Bye world" 102 | - cmd: echo "Hello again world" 103 | -------------------------------------------------------------------------------- /test/playground/tasks/tasks.yaml: -------------------------------------------------------------------------------- 1 | tasks: 2 | pwd: pwd 3 | 4 | # specs: 5 | # table: 6 | # output: table 7 | # # describe: true 8 | # silent: true 9 | 10 | # themes: 11 | # kaka: 12 | # text: 13 | # # prefix: '{{ .Index }}' 14 | # # prefix: '{{ .Index }} @ {{ .Name }} @ {{ .Host }}:{{ .Port }} : {{ .User }}' 15 | # # prefix: "{{ .Name }}" 16 | # # prefix: "{{ .Host }}" 17 | # # prefix: "{{ .Host }}:{{ .Port }}" 18 | # header: '{{ .Style "TASK" "bold" }}{{ if ne .NumTasks 1 }} ({{ .Index }}/{{ .NumTasks }}){{end}}{{ if and .Name .Desc }} [{{.Style .Name "bold"}}: {{ .Desc }}] {{ else if .Name }} [{{ .Name }}] {{ else if .Desc }} [{{ .Desc }}] {{end}}' 19 | # table: 20 | # prefix: "{{ .Host }}:{{ .Port }}" 21 | -------------------------------------------------------------------------------- /test/playground/work-dir/sake.yaml: -------------------------------------------------------------------------------- 1 | disable_verify_host: true 2 | 3 | servers: 4 | localhost: 5 | desc: localhost 6 | host: localhost 7 | local: true 8 | tags: [local] 9 | work_dir: /tmp 10 | 11 | server-1: 12 | desc: hosts mealie, node-red 13 | host: server-1.lan 14 | tags: [remote, pi] 15 | work_dir: /home 16 | 17 | pihole: 18 | desc: runs pihole 19 | host: pihole.lan 20 | tags: [remote, pi] 21 | 22 | tasks: 23 | work-ref: 24 | name: ref 25 | work_dir: "/usr" # 3 26 | cmd: pwd 27 | 28 | work-nested: 29 | name: nested 30 | # work_dir: "/sys" 31 | tasks: 32 | - task: work-ref 33 | 34 | work-dir-1: 35 | spec: 36 | output: table 37 | target: 38 | all: true 39 | work_dir: "/home" # 2 40 | tasks: 41 | - task: work-ref 42 | 43 | - task: work-ref 44 | name: Override inline ref 45 | work_dir: /opt 46 | 47 | - cmd: pwd 48 | name: Inline 49 | 50 | - cmd: pwd 51 | name: Override inline 52 | work_dir: "/" # 1 53 | 54 | work-dir-2: 55 | spec: 56 | output: table 57 | target: 58 | all: true 59 | tasks: 60 | - task: work-ref 61 | 62 | - task: work-ref 63 | name: Override inline ref 64 | work_dir: /opt 65 | 66 | - cmd: pwd 67 | name: Inline 68 | 69 | - cmd: pwd 70 | name: Override inline 71 | work_dir: "/" # 1 72 | 73 | work-dir-3: 74 | spec: 75 | output: table 76 | target: 77 | all: true 78 | tasks: 79 | - task: work-nested 80 | 81 | - task: work-nested 82 | work_dir: /etc 83 | -------------------------------------------------------------------------------- /test/profiles/list-servers: -------------------------------------------------------------------------------- 1 | Benchmark 1: ../dist/sake list servers 2 | Time (mean ± σ): 5.2 ms ± 0.2 ms [User: 3.5 ms, System: 2.1 ms] 3 | Range (min … max): 4.9 ms … 5.5 ms 10 runs 4 | 5 | -------------------------------------------------------------------------------- /test/profiles/nested: -------------------------------------------------------------------------------- 1 | Benchmark 1: ../dist/sake run d --forks=1 -t reachable 2 | Time (mean ± σ): 544.8 ms ± 21.1 ms [User: 408.1 ms, System: 62.8 ms] 3 | Range (min … max): 505.6 ms … 568.8 ms 10 runs 4 | 5 | -------------------------------------------------------------------------------- /test/profiles/nested-parallel: -------------------------------------------------------------------------------- 1 | Benchmark 1: ../dist/sake run d --strategy=free -t reachable 2 | Time (mean ± σ): 452.9 ms ± 36.7 ms [User: 364.3 ms, System: 34.0 ms] 3 | Range (min … max): 416.8 ms … 525.4 ms 10 runs 4 | 5 | -------------------------------------------------------------------------------- /test/profiles/ping: -------------------------------------------------------------------------------- 1 | Benchmark 1: ../dist/sake run ping --forks=1 -t reachable 2 | Time (mean ± σ): 578.6 ms ± 22.1 ms [User: 351.1 ms, System: 31.7 ms] 3 | Range (min … max): 552.7 ms … 613.5 ms 10 runs 4 | 5 | -------------------------------------------------------------------------------- /test/profiles/ping-no-key: -------------------------------------------------------------------------------- 1 | Benchmark 1: ../dist/sake run ping -s server-9 2 | Time (mean ± σ): 139.8 ms ± 31.8 ms [User: 8.7 ms, System: 9.4 ms] 3 | Range (min … max): 106.4 ms … 199.5 ms 10 runs 4 | 5 | -------------------------------------------------------------------------------- /test/profiles/ping-parallel: -------------------------------------------------------------------------------- 1 | Benchmark 1: ../dist/sake run ping --strategy=free -t reachable 2 | Time (mean ± σ): 548.0 ms ± 40.0 ms [User: 324.8 ms, System: 39.7 ms] 3 | Range (min … max): 471.7 ms … 603.9 ms 10 runs 4 | 5 | -------------------------------------------------------------------------------- /test/sake.yaml: -------------------------------------------------------------------------------- 1 | disable_verify_host: true 2 | 3 | import: 4 | - ./servers.yaml 5 | 6 | env: 7 | NO_COLOR: true 8 | 9 | targets: 10 | all: 11 | all: true 12 | 13 | specs: 14 | default: 15 | output: table 16 | strategy: linear 17 | batch: 1 18 | 19 | table: 20 | output: table 21 | 22 | text: 23 | output: text 24 | 25 | info: 26 | output: table 27 | strategy: free 28 | ignore_errors: true 29 | ignore_unreachable: true 30 | any_errors_fatal: false 31 | 32 | themes: 33 | default: 34 | text: 35 | prefix: "{{ .Host }}" 36 | header: '{{ .Style "TASK" "bold" }}{{ if ne .NumTasks 1 }} ({{ .Index }}/{{ .NumTasks }}){{end}}{{ if and .Name .Desc }} [{{.Style .Name "bold"}}: {{ .Desc }}] {{ else if .Name }} [{{ .Name }}] {{ else if .Desc }} [{{ .Desc }}] {{end}}' 37 | table: 38 | prefix: "{{ .Host }}" 39 | 40 | tasks: 41 | ping: 42 | target: all 43 | desc: ping server 44 | cmd: echo pong 45 | -------------------------------------------------------------------------------- /test/servers.yaml: -------------------------------------------------------------------------------- 1 | import: 2 | - ./tasks.yaml 3 | 4 | servers: 5 | localhost: 6 | desc: localhost 7 | host: localhost 8 | user: test 9 | local: true 10 | work_dir: /tmp 11 | tags: [local, reachable] 12 | 13 | unreachable: 14 | host: 172.24.2.50 15 | user: test 16 | tags: [unreachable] 17 | 18 | list: 19 | desc: many hosts using list 20 | hosts: 21 | - 172.24.2.2 22 | - test@172.24.2.4:22 23 | user: test 24 | identity_file: keys/id_ed25519_pem 25 | password: testing 26 | tags: [remote, prod, list, reachable] 27 | env: 28 | hello: world 29 | 30 | range: 31 | desc: many hosts using range 32 | hosts: 172.24.2.[2:4:2] 33 | user: test 34 | password: test 35 | tags: [remote, prod, range, reachable] 36 | env: 37 | hello: world 38 | 39 | inv: 40 | desc: many hosts using inventory 41 | inventory: echo "172.24.2.2 test@$host:22" 42 | user: test 43 | password: test 44 | tags: [remote, prod, inv, reachable] 45 | env: 46 | hello: world 47 | host: 172.24.2.4 48 | 49 | server-1: 50 | desc: server-1 51 | host: 172.24.2.2 52 | user: test 53 | work_dir: /home/test 54 | identity_file: keys/id_ed25519_pem 55 | password: testing 56 | tags: [remote, prod, reachable] 57 | env: 58 | host: 172.24.2.2 59 | 60 | server-2: 61 | desc: server-2 62 | host: 172.24.2.3 63 | user: test 64 | port: 33 65 | identity_file: keys/id_ed25519_pem_no 66 | tags: [remote, prod, reachable] 67 | 68 | server-3: 69 | desc: server-3 70 | host: 172.24.2.4 71 | user: test 72 | identity_file: keys/id_ed25519_rfc 73 | password: testing 74 | tags: [remote, demo, reachable] 75 | 76 | server-4: 77 | desc: server-4 78 | host: 172.24.2.5 79 | user: test 80 | identity_file: keys/id_ed25519_rfc_no 81 | tags: [remote, demo, reachable] 82 | 83 | server-5: 84 | desc: server-5 85 | host: 172.24.2.6 86 | user: test 87 | identity_file: keys/id_rsa_pem 88 | password: testing 89 | tags: [remote, sandbox, reachable] 90 | 91 | server-6: 92 | desc: server-6 93 | host: 172.24.2.7 94 | user: test 95 | identity_file: keys/id_rsa_pem_no 96 | tags: [remote, sandbox, reachable] 97 | 98 | server-7: 99 | desc: server-7 100 | host: test@172.24.2.8:22 101 | identity_file: keys/id_rsa_rfc 102 | password: testing 103 | tags: [remote, demo, reachable] 104 | 105 | server-8: 106 | desc: server-8 107 | host: 172.24.2.9 108 | user: test 109 | identity_file: keys/id_rsa_rfc_no 110 | tags: [remote, demo, reachable] 111 | 112 | server-9: 113 | desc: server-9 114 | host: 2001:3984:3989::10 115 | user: test 116 | password: test 117 | tags: [remote, demo, reachable] 118 | 119 | server-10: 120 | desc: server-10 desc 121 | host: test@172.24.2.10:22 122 | bastion: test@172.24.2.98:22 123 | identity_file: keys/id_rsa_rfc 124 | password: testing 125 | tags: [remote, bastion] 126 | work_dir: /tmp 127 | 128 | server-11: 129 | desc: server-11 desc 130 | host: test@172.24.2.11:22 131 | bastions: ['test@172.24.2.98:22', 'test@172.24.2.99:22'] 132 | identity_file: keys/id_rsa_rfc 133 | password: testing 134 | tags: [remote, bastion] 135 | work_dir: /tmp 136 | -------------------------------------------------------------------------------- /test/user-config.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alajmo/sake/9547a4fff6ab53e2b3abec4fe8a2da70629b199f/test/user-config.yaml --------------------------------------------------------------------------------