├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── release.yml
│ └── tests.yml
├── .gitignore
├── .gitlab-ci.yml
├── .goreleaser.yaml
├── LICENSE
├── README.md
├── cmd
├── apply.go
├── connect.go
├── edit.go
├── init.go
└── root.go
├── docs
├── license.tpl
└── sshabu-quick.gif
├── go.mod
├── go.sum
├── main.go
├── pkg
├── checker.go
├── checker_test.go
├── compare
│ ├── compare.go
│ └── compare_test.go
├── openssh_conf.gtpl
├── sshabu_example.yaml
├── template.go
├── template_test.go
├── types.go
└── types_test.go
├── scripts
└── completion.sh
└── third_party_licenses
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: 'Bug: Name'
5 | labels: ''
6 | assignees: alvtsky
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: 'FeatureRequest: Your Name'
5 | labels: ''
6 | assignees: alvtsky
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # .github/workflows/release.yml
2 | name: goreleaser
3 |
4 | on:
5 | push:
6 | tags:
7 | - "*"
8 |
9 | permissions:
10 | contents: write
11 | packages: write
12 | # issues: write
13 |
14 | jobs:
15 | goreleaser:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v3
19 | with:
20 | fetch-depth: 0
21 | - run: git fetch --force --tags
22 | - uses: actions/setup-go@v4
23 | with:
24 | go-version: stable
25 | # - run: chmod +x ./scripts/completion.sh
26 | - uses: goreleaser/goreleaser-action@v5
27 | with:
28 | distribution: goreleaser
29 | version: latest
30 | args: release --clean
31 | env:
32 | GITHUB_TOKEN: ${{ secrets.GH_PAT }}
33 | # TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
34 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Go Tests with Coverage
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout code
10 | uses: actions/checkout@v4
11 | with:
12 | fetch-depth: 0 # Important for some Go tools that need full history
13 |
14 | - name: Set up Go
15 | uses: actions/setup-go@v4
16 | with:
17 | go-version: '1.21' # Or your preferred version
18 | check-latest: true # Ensures you get the latest patch version
19 |
20 | - name: Cache Go modules
21 | uses: actions/cache@v3
22 | with:
23 | path: |
24 | ~/go/pkg/mod
25 | ${GITHUB_WORKSPACE}/.go-cache
26 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
27 | restore-keys: |
28 | ${{ runner.os }}-go-
29 |
30 | - name: Run tests (recursive)
31 | run: |
32 | # Print Go version for debugging
33 | go version
34 |
35 | # Verify we're finding all test files
36 | echo "Test files found:"
37 | find . -name "*_test.go" | sed 's/^/ /'
38 |
39 | # Run tests with coverage (./... means recursive)
40 | go test -v -coverprofile=coverage.out -covermode=atomic ./...
41 |
42 | # Generate coverage report
43 | go tool cover -func=coverage.out
44 | go tool cover -html=coverage.out -o coverage.html
45 |
46 | - name: Upload coverage report
47 | uses: actions/upload-artifact@v4
48 | with:
49 | name: go-coverage-report
50 | path: |
51 | coverage.out
52 | coverage.html
53 |
54 | # # Optional: Upload to codecov.io
55 | # - name: Upload to Codecov
56 | # if: success() # Only run if tests passed
57 | # uses: codecov/codecov-action@v3
58 | # with:
59 | # token: ${{ secrets.CODECOV_TOKEN }} # Only needed for private repos
60 | # file: coverage.out
61 | # flags: unittests
62 | # name: go-coverage
63 | # fail_ci_if_error: false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | openssh.*
2 | sshabu.yaml
3 | *.drowio
4 | *.bkp
5 | dist/
6 | .DS_Store
7 | completions
8 | .vscode/
9 | sshabu
10 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | .build:
2 | image: golang:1.21-alpine3.17
3 | tags:
4 | - docker
5 | - walnut
6 | variables:
7 | GO_PROJECT: sshabu
8 |
9 | stages:
10 | - building
11 |
12 |
13 | build:
14 | stage: building
15 | extends: .build
16 | script:
17 | - GOOS=linux GOARCH=amd64 go build -o $GO_PROJECT-linux-amd64
18 | # - GOOS=linux GOARCH=386 go build -o $GO_PROJECT-linux-386
19 | # - GOOS=windows GOARCH=amd64 go build -o $GO_PROJECT-windows-amd64
20 | # - GOOS=windows GOARCH=386 go build -o $GO_PROJECT-windows-386
21 | - GOOS=darwin GOARCH=arm64 go build -o $GO_PROJECT-darwin-arm64
22 | artifacts:
23 | paths:
24 | - $GO_PROJECT-linux-amd64
25 | - $GO_PROJECT-darwin-arm64
26 | rules:
27 | - if: $CI_COMMIT_REF_NAME == "main"
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # This is an example .goreleaser.yml file with some sensible defaults.
2 | # Make sure to check the documentation at https://goreleaser.com
3 |
4 | # The lines below are called `modelines`. See `:help modeline`
5 | # Feel free to remove those if you don't want/need to use them.
6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json
7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj
8 |
9 | version: 1
10 |
11 | before:
12 | hooks:
13 | # You may remove this if you don't use go modules.
14 | - go mod tidy
15 | # you may remove this if you don't need go generate
16 | - go generate ./...
17 |
18 | # - ./scripts/completion.sh
19 |
20 | builds:
21 | - env:
22 | - CGO_ENABLED=0
23 | goos:
24 | - linux
25 | # - windows
26 | - darwin
27 |
28 | archives:
29 | - format: tar.gz
30 | # this name template makes the OS and Arch compatible with the results of `uname`.
31 | name_template: >-
32 | {{ .ProjectName }}_
33 | {{- title .Os }}_
34 | {{- if eq .Arch "amd64" }}x86_64
35 | {{- else if eq .Arch "386" }}i386
36 | {{- else }}{{ .Arch }}{{ end }}
37 | {{- if .Arm }}v{{ .Arm }}{{ end }}
38 | # use zip for windows archives
39 | format_overrides:
40 | - goos: windows
41 | format: zip
42 |
43 | changelog:
44 | use: git
45 | sort: asc
46 | abbrev: -1
47 |
48 | filters:
49 | exclude:
50 | - "^docs:"
51 | - "^test:"
52 | include:
53 | - "^feat:"
54 | - "^bug:"
55 | brews:
56 | -
57 | # NOTE: make sure the url_template, the token and given repo (github or
58 | # gitlab) owner and name are from the same kind.
59 | # We will probably unify this in the next major version like it is
60 | # done with scoop.
61 |
62 | # URL which is determined by the given Token (github, gitlab or gitea).
63 | #
64 | # Default depends on the client.
65 | # Templates: allowed
66 | url_template: "https://github.com/ratsky-oss/sshabu/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
67 |
68 | # Caveats for the user of your binary.
69 | caveats: "Use `sshabu init` to create configuration example in $HOME/.sshabu/ directory"
70 |
71 | # Your app's homepage.
72 | homepage: "https://github.com/ratsky-oss/sshabu"
73 |
74 | # Your app's description.
75 | #
76 | # Templates: allowed
77 | description: "SSH client management tool"
78 |
79 | # SPDX identifier of your app's license.
80 | license: "Apache-2.0"
81 |
82 | # Setting this will prevent goreleaser to actually try to commit the updated
83 | # formula - instead, the formula file will be stored on the dist folder only,
84 | # leaving the responsibility of publishing it to the user.
85 | # If set to auto, the release will not be uploaded to the homebrew tap
86 | # in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1
87 | #
88 | # Templates: allowed
89 | skip_upload: auto
90 |
91 | # Packages your package depends on.
92 | dependencies:
93 | - name: openssh
94 | type: optional
95 |
96 | # Custom install script for brew.
97 | #
98 | # Template: allowed
99 | # Default: 'bin.install "BinaryName"'
100 | install: |
101 | bin.install "sshabu"
102 |
103 | # Additional install instructions so you don't need to override `install`.
104 | #
105 | # Template: allowed
106 | # Since: v1.20.
107 | # extra_install: |
108 | # bash_completion.install "completions/sshabu.bash" => "sshabu"
109 | # zsh_completion.install "completions/sshabu.zsh" => "_sshabu"
110 | # fish_completion.install "completions/sshabu.fish"
111 |
112 | # Repository to push the generated files to.
113 | repository:
114 | owner: ratsky-oss
115 | name: homebrew-taps
116 | # token: "{{ .Env.GITHUB_HOMEBREW_AUTH_TOKEN }}"
117 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ratsky Sshabu
2 |
3 |
4 | # Table of contents
5 |
6 | - [Overview](#overview)
7 | - [Quick start](#quick-start)
8 | - [Installation](#installation)
9 | - [Commands](#commands)
10 | - [Configuration](#configuration)
11 | - [License](#license)
12 | - [Contacts](#contact)
13 |
14 | # Overview
15 |
16 | `Ratsky Sshabu` is a robust SSH client management tool designed to streamline the process of connecting to multiple servers effortlessly. This tool leverages OpenSSH and offers a user-friendly interface to enhance the overall SSH experience. With Sshabu, managing SSH configurations becomes more intuitive, allowing users to organize and connect to their servers efficiently.
17 |
18 |
19 |
20 | > Openssh should be installed on your system.
21 |
22 | # Quick start
23 | 1. Install via brew
24 | ```bash
25 | brew install ratsky-oss/taps/sshabu
26 | ```
27 | > Or download binary and move in $PATH by yourself
28 | > - Download the binary file `sshabu`. You can view them on > the release page:
29 | > https://github.com/Ra-sky/sshabu/releases
30 | > ```bash
31 | > wget https://github.com/ratsky-oss/sshabu/releases/download/v0.1.2/sshabu_Linux_arm64.tar.gz
32 | > ```
33 | > - Unzip and move binary file `sshabu` to `/usr/local/bin/`.
34 | > ```bash
35 | > mkdir sshabu_Darwin_arm64 && tar -xvzf sshabu_Darwin_arm64.> tar.gz -C sshabu_Darwin_arm64 && \
36 | > cd sshabu_Darwin_arm64 && \
37 | > mv sshabu /usr/local/bin/sshabu
38 | > ```
39 | 2. Initialize your sshabu configuration.
40 | ```bash
41 | sshabu init
42 | ```
43 | 3. Enable auto-completion. Several options are available; use the following command to view them:
44 | ```bash
45 | sshabu completion --help
46 | ```
47 | 4. Begin editing the config with this convenient command:
48 | ```bash
49 | sshabu edit
50 | ```
51 | 5. Connect to servers by specifying the name.
52 | ```bash
53 | sshabu connect
54 | ```
55 | 6. Yep-Yep-Yep! It's time for shabu!
56 |
57 | # Installation
58 | ## Brew
59 | 1. Adding Ratsky third-party repository
60 | ```bash
61 | brew tap ratsky-oss/taps
62 | ```
63 | 2. Installing sshabu
64 | ```
65 | brew install sshabu
66 | ```
67 | 3. Validate sshabu binary
68 | ```
69 | which sshabu
70 | ```
71 | 4. Initialize your `sshabu` configuration.
72 | ```bash
73 | sshabu init
74 | ```
75 | ## Easy
76 | 1. Download the binary file `sshabu` to `/usr/bin/sshabu`. You can change the default path from `/usr/bin/sshabu` to your own, but make sure that the path is included in your `PATH` environment variable.
77 |
80 | 2. Initialize your `sshabu` configuration.
81 | ```bash
82 | sshabu init
83 | ```
84 | ## Build from source
85 | 1. Clone the git repository.
86 |
89 | 2. Change the directory to the cloned project.
90 | ```bash
91 | cd ./sshabu
92 | ```
93 | 3. Build the project.
94 | ```bash
95 | go build .
96 | ```
97 | 4. Move the binary file `sshabu`. You can change the default path from `/usr/bin/sshabu` to your own, but make sure that the path is included in your `PATH` environment variable.
98 | ```bash
99 | mv sshabu /usr/bin/sshabu
100 | ```
101 | 5. Initialize your `sshabu` configuration.
102 | ```bash
103 | sshabu init
104 | ```
105 |
106 | ## Commands
107 |
108 | #### `sshabu init`
109 | Create `~/$HOME/.sshabu/` directory and generate example `sshabu.yaml` config
110 | #### `sshabu apply`
111 | Generate `openssh.config` based on `sshabu.yaml`
112 | #### `sshabu edit`
113 | Open `sshabu.yaml` with editor and runs `sshabu apply after that`
114 | #### `sshabu connect`
115 | Runs openssh command with `openssh.config`
116 |
117 | > Find out more info by using `--help` flag
118 |
119 | ## Configuration
120 |
121 | All unmentioned options will be inherited from parent(s) groups 'till top group in derictive
122 |
123 | > ~/.sshabu/sshabu.yaml
124 |
125 | Config structure
126 |
127 | ```
128 | GlobalOptions:
129 |
130 |
131 | Groups:
132 | [- ]
133 |
134 | Hosts:
135 | [- ]
136 | ```
137 |
138 |
139 | ### \
140 |
141 | Target of ssh configuration
142 |
143 | > All \.Name(s) must be unique
144 |
145 | ```
146 | Name:
147 |
148 | ```
149 | Name field is an identifire of following config. \
150 | Name field value will be used as "Host" derective in final Openssh config, if no "Host" option in is defined.
151 |
152 | ### \
153 |
154 | Groups allow you to define some options for all included entities such s \ in "Hosts" and \ in "Subgroups".
155 |
156 | Groups was designed to provide option inheritance
157 |
158 | Option precedence list (higher option will override lower):
159 | - \.option
160 | - \.\.option
161 | ...
162 | - \.\....option
163 | ...
164 | - GlobalOption
165 |
166 | ```
167 | Name:
168 | Hosts:
169 | - []
170 | Options:
171 |
172 | Subgroups:
173 | - []
174 | ```
175 |
176 | ### \
177 |
178 | Generally just openssh config options in "key: value" format\
179 | man page - ssh_config(5)
180 | https://linux.die.net/man/5/ssh_config \
181 |
182 | > Avoid using 'Host' option unless you know what you are doing.
183 |
184 | ```
185 | HostName:
186 | IdentityFile:
187 | StrictHostKeyChecking:
188 | ...
189 | ```
190 | > Suggestion - "Host" better be unique if used \
191 | > Considerations: \
192 | > Host wildcards?? \
193 | > Using "Host" inside:
194 | > - "GlabalOptions" is useless
195 | > - "\" will override \."Name" section in destination openssh config
196 |
197 |
198 | All options reference
199 |
200 | |Openssh option | Sshabu | Tested |
201 | |---|:---:|:---:|
202 | | AddKeysToAgent | ✅ | ❌ |
203 | | AddressFamily | ✅ | ❌ |
204 | | BatchMode | ✅ | ❌ |
205 | | BindAddress | ✅ | ❌ |
206 | | CanonicalDomains | ✅ | ❌ |
207 | | CanonicalizeFallbackLocal | ✅ | ❌ |
208 | | CanonicalizeHostname | ✅ | ❌ |
209 | | CanonicalizeMaxDots | ✅ | ❌ |
210 | | CanonicalizePermittedCNAMEs | ✅ | ❌ |
211 | | CASignatureAlgorithms | ✅ | ❌ |
212 | | CertificateFile | ✅ | ❌ |
213 | | CheckHostIP | ✅ | ❌ |
214 | | Ciphers | ✅ | ❌ |
215 | | ClearAllForwardings | ✅ | ❌ |
216 | | Compression | ✅ | ❌ |
217 | | ConnectionAttempts | ✅ | ❌ |
218 | | ConnectTimeout | ✅ | ❌ |
219 | | ControlMaster | ✅ | ❌ |
220 | | ControlPath | ✅ | ❌ |
221 | | ControlPersist | ✅ | ❌ |
222 | | DynamicForward | ✅ | ❌ |
223 | | EnableEscapeCommandline | ❌ | ❌ |
224 | | EscapeChar | ✅ | ❌ |
225 | | ExitOnForwardFailure | ✅ | ❌ |
226 | | FingerprintHash | ✅ | ❌ |
227 | | ForkAfterAuthentication | ✅ | ❌ |
228 | | ForwardAgent | ✅ | ❌ |
229 | | ForwardX11 | ✅ | ❌ |
230 | | ForwardX11Timeout | ✅ | ❌ |
231 | | ForwardX11Trusted | ✅ | ❌ |
232 | | GatewayPorts | ✅ | ❌ |
233 | | GlobalKnownHostsFile | ✅ | ❌ |
234 | | GSSAPIAuthentication | ✅ | ❌ |
235 | | GSSAPIDelegateCredentials | ✅ | ❌ |
236 | | HashKnownHosts | ✅ | ❌ |
237 | | Host | ✅ | ✅ |
238 | | HostbasedAcceptedAlgorithms | ✅ | ❌ |
239 | | HostbasedAuthentication | ✅ | ❌ |
240 | | HostKeyAlgorithms | ✅ | ❌ |
241 | | HostKeyAlias | ✅ | ❌ |
242 | | Hostname | ✅ | ✅ |
243 | | IdentitiesOnly | ✅ | ❌ |
244 | | IdentityAgent | ✅ | ❌ |
245 | | IdentityFile | ✅ | ✅ |
246 | | IPQoS | ✅ | ❌ |
247 | | KbdInteractiveAuthentication | ❌ | ❌ |
248 | | KbdInteractiveDevices | ✅ | ❌ |
249 | | KexAlgorithms | ✅ | ❌ |
250 | | KnownHostsCommand | ✅ | ❌ |
251 | | LocalCommand | ✅ | ❌ |
252 | | LocalForward | ✅ | ❌ |
253 | | LogLevel | ✅ | ❌ |
254 | | MACs | ✅ | ❌ |
255 | | Match | ❌ | ❌ |
256 | | NoHostAuthenticationForLocalhost | ✅ | ❌ |
257 | | NumberOfPasswordPrompts | ✅ | ❌ |
258 | | PasswordAuthentication | ✅ | ❌ |
259 | | PermitLocalCommand | ✅ | ❌ |
260 | | PermitRemoteOpen | ✅ | ❌ |
261 | | PKCS11Provider | ✅ | ❌ |
262 | | Port | ✅ | ✅ |
263 | | PreferredAuthentications | ✅ | ❌ |
264 | | ProxyCommand | ✅ | ❌ |
265 | | ProxyJump | ✅ | ❌ |
266 | | ProxyUseFdpass | ✅ | ❌ |
267 | | PubkeyAcceptedAlgorithms | ✅ | ❌ |
268 | | PubkeyAuthentication | ✅ | ❌ |
269 | | RekeyLimit | ✅ | ❌ |
270 | | RemoteCommand | ✅ | ❌ |
271 | | RemoteForward | ✅ | ❌ |
272 | | RequestTTY | ✅ | ❌ |
273 | | RequiredRSASize | ❌ | ❌ |
274 | | SendEnv | ✅ | ❌ |
275 | | ServerAliveInterval | ✅ | ❌ |
276 | | ServerAliveCountMax | ✅ | ❌ |
277 | | SessionType | ✅ | ❌ |
278 | | SetEnv | ✅ | ❌ |
279 | | StdinNull | ✅ | ❌ |
280 | | StreamLocalBindMask | ✅ | ❌ |
281 | | StreamLocalBindUnlink | ✅ | ❌ |
282 | | StrictHostKeyChecking | ✅ | ✅ |
283 | | TCPKeepAlive | ✅ | ❌ |
284 | | Tunnel | ✅ | ❌ |
285 | | TunnelDevice | ✅ | ❌ |
286 | | UpdateHostKeys | ✅ | ❌ |
287 | | UseKeychain | ✅ | ❌ |
288 | | User | ✅ | ✅ |
289 | | UserKnownHostsFile | ✅ | ❌ |
290 | | VerifyHostKeyDNS | ✅ | ❌ |
291 | | VisualHostKey | ✅ | ❌ |
292 | | XAuthLocation | ✅ | ❌ |
293 |
294 |
295 |
296 |
297 | Coniguration example
298 |
299 | ```
300 | # ----------------------------------------------------------------------
301 | # Default options for all hosts
302 |
303 | GlobalOptions:
304 | LogLevel: INFO
305 | User: user
306 | IdentityFile: /.ssh/id_rsa
307 |
308 | # ----------------------------------------------------------------------
309 | # Top level standalone host list
310 |
311 | Hosts:
312 | - Name: smth_ungrouped # Host example
313 | HostName: host.example.com # Key: value ssh_config(5)
314 | User: user2
315 | IdentityFile: /path/to/key2
316 | Port: 2223
317 |
318 | # ----------------------------------------------------------------------
319 |
320 | # Top level group list
321 | Groups:
322 | - Name: work # Some group
323 | Hosts: # List of Target host
324 | - Name: project1-lab # This host will inherit User, IdentityFile from group "work"
325 | HostName: lab.project1.ratsky.local
326 | Options: # ssh_config(5)
327 | User: alivitskiy
328 | IdentityFile: /.ssh/id_rsa.work
329 | Subgroups: # List of subgroups that will inherit "work" Options
330 |
331 | - Name: prod
332 | Options:
333 | IdentityFile: /.ssh/id_rsa.work2
334 | Hosts:
335 | - Name: project1
336 | HostName: 192.168.1.2
337 | Port: 2222
338 |
339 | - Name: test
340 | Hosts:
341 | - Name: project1-test
342 | HostName: 192.168.11.3
343 |
344 | - Name: home # Another group
345 | Hosts:
346 | - Name: home-gitlab
347 | HostName: gitlab.ratsky.local
348 | - Name: home-nextcloud
349 | HostName: nc.ratsky.local
350 |
351 | # ----------------------------------------------------------------------
352 | ```
353 |
354 | Result
355 | ```
356 | # sshabu apply
357 |
358 | IdentityFile /.ssh/id_rsa
359 | LogLevel INFO
360 |
361 | Host smth_ungrouped
362 | Hostname host.example.com
363 | IdentityFile /path/to/key2
364 | Port 2223
365 |
366 | Host project1-lab
367 | Hostname lab.project1.ratsky.local
368 | IdentityFile /.ssh/id_rsa.work
369 |
370 | Host project1
371 | Hostname 192.168.1.2
372 | IdentityFile /.ssh/id_rsa.work2
373 | Port 2222
374 |
375 | Host project1-test
376 | Hostname 192.168.11.3
377 | IdentityFile /.ssh/id_rsa.work
378 |
379 | Host home-gitlab
380 | Hostname gitlab.ratsky.local
381 |
382 | Host home-nextcloud
383 | Hostname nc.ratsky.local
384 |
385 | ```
386 |
387 |
388 |
389 | # License
390 |
391 | Ratsky Sshabu is released under the Apache 2.0 license. See LICENSE.
392 |
393 | # Contact
394 |
395 | If you have any questions or feedback about the Ratsky Sshabu, please contact us at 28xxgs3im@mozmail.com. We would be pleased to receive your feedback!
--------------------------------------------------------------------------------
/cmd/apply.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package cmd
15 |
16 | import (
17 | "bytes"
18 | "fmt"
19 | "os"
20 | "sshabu/pkg"
21 | "sshabu/pkg/compare"
22 | "strings"
23 |
24 | "github.com/spf13/cobra"
25 | "github.com/spf13/viper"
26 | )
27 |
28 | func RunApply(args []string) error {
29 | // Create a fresh command instance
30 | runApplyCmd := &cobra.Command{
31 | Use: applyCmd.Use,
32 | Run: applyCmd.Run,
33 | }
34 | // Copy all flags
35 | runApplyCmd.Flags().AddFlagSet(applyCmd.Flags())
36 | runApplyCmd.SetArgs(args)
37 | if err := viper.ReadInConfig(); err != nil{
38 | return err
39 | }
40 |
41 | err := runApplyCmd.Execute()
42 | return err
43 | }
44 | // applyCmd represents the apply command
45 | var applyCmd = &cobra.Command{
46 | Use: "apply",
47 | Short: "Transform sshabu.yaml to openssh.config",
48 | Long: `Apply the generate openssh_config according to yaml specification.
49 | Command is going to ask you confirmation before overriding destination openssh.config.
50 | openssh.config file is located right next to the used sshabu.yaml`,
51 | Run: func(cmd *cobra.Command, args []string) {
52 |
53 | fmt.Println("⸫ Using config file:", cfgFile)
54 |
55 | var shabu sshabu.Shabu
56 | err := viper.UnmarshalExact(&shabu)
57 | cobra.CheckErr(err)
58 | // if shabu.AreAllUnique(){
59 | // fmt.Println("YAML seems OK")
60 | // } else {
61 | // fmt.Println("Error: 'Name' Fields must be unique")
62 | // os.Exit(1)
63 | // }
64 | // names := sshabu.FindNamesInShabu(shabu)
65 |
66 | err = shabu.Boil()
67 | cobra.CheckErr(err)
68 |
69 | buf := new(bytes.Buffer)
70 | err = sshabu.RenderTemplate(shabu, buf)
71 | cobra.CheckErr(err)
72 |
73 | err = os.WriteFile(opensshTmpFile, buf.Bytes(), 0600)
74 | cobra.CheckErr(err)
75 | sshabu.OpensshCheck(opensshTmpFile)
76 |
77 | var (
78 | destFile compare.Bites
79 | tmpFile compare.Bites
80 | )
81 |
82 | destFile.TakeBites(opensshDestconfigFile)
83 | tmpFile.TakeBites(opensshTmpFile)
84 |
85 | differences := compare.DiffBites(destFile, tmpFile)
86 |
87 | if len(differences) == 0{
88 | fmt.Println("---------------------")
89 | fmt.Println("No changes! ʕっ•ᴥ•ʔっ")
90 | fmt.Println("---------------------")
91 | return
92 | }
93 |
94 |
95 |
96 | if !forceApply {
97 |
98 | resultStrings := compare.TransformDifferencesToReadableFormat(differences, destFile, tmpFile)
99 |
100 | for _,line := range(resultStrings) {
101 | fmt.Println(line)
102 | }
103 |
104 | fmt.Println("\nDo you really want to apply changes? (yes/no): ")
105 | if !sshabu.AskForConfirmation() {
106 | fmt.Println("Aborted")
107 | return
108 | }
109 | }
110 |
111 | err = os.WriteFile(opensshDestconfigFile, []byte(strings.Join(tmpFile.Content, "\n")), 0644)
112 | os.Remove(opensshTmpFile)
113 | if err != nil {
114 | fmt.Println("Error overwriting the file:", err)
115 | return
116 | }
117 | fmt.Println("Yep-Yep-Yep! It's time for shabu! ʕ •́؈•̀)")
118 | },
119 | }
120 |
121 | var forceApply bool
122 |
123 | func init() {
124 | applyCmd.Flags().BoolVarP(&forceApply, "force", "f", false, "Apply configuration without confirmation")
125 | rootCmd.AddCommand(applyCmd)
126 | }
--------------------------------------------------------------------------------
/cmd/connect.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package cmd
15 |
16 | import (
17 | "fmt"
18 | "os"
19 | "os/exec"
20 | sshabu "sshabu/pkg"
21 |
22 | "github.com/spf13/cobra"
23 | )
24 |
25 | // connectCmd represents the connect command
26 | var connectCmd = &cobra.Command{
27 | Use: "connect [flags] [user@]name_of_host",
28 | Short: "Just a wrapper around ssh command",
29 | Long: `Generally just a wrapper around ssh command with autocompletion from sshabu config.
30 |
31 | Base usage:
32 | ~ sshabu connect some_host
33 | # Command above wll be transformed to the following
34 | # ssh -F $HOME/.sshabu/openssh.config some_host
35 |
36 | Optionally you could pass openssh parametrs or override user
37 | ~ sshabu connect -o "-p 2222 -i /path/to/dir" user@host_example
38 | # ssh -F $HOME/.sshabu/openssh.config -p 2222 -i /path/to/dir user@host_example
39 | `,
40 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
41 | if len(args) != 0 {
42 | return nil, cobra.ShellCompDirectiveNoFileComp
43 | }
44 |
45 | file, _ := os.Open(opensshDestconfigFile)
46 |
47 | defer file.Close()
48 |
49 | hostValues, err := sshabu.DestinationHosts(file)
50 | if err != nil {
51 | file.Close()
52 | return nil, cobra.ShellCompDirectiveNoFileComp
53 | }
54 | return hostValues, cobra.ShellCompDirectiveNoFileComp
55 | },
56 | Run: func(cmd *cobra.Command, args []string) {
57 | // Construct the ssh command with -I option
58 |
59 | args = append(args, extraOptions)
60 |
61 | sshArgs := append([]string{"-F", opensshDestconfigFile}, args...)
62 |
63 | fmt.Println("Running SSH command:", "ssh", sshArgs)
64 |
65 | // Execute the SSH command
66 | scmd := exec.Command("ssh", sshArgs...)
67 | scmd.Stdout = os.Stdout
68 | scmd.Stderr = os.Stderr
69 | scmd.Stdin = os.Stdin
70 | if err := scmd.Run(); err != nil {
71 | fmt.Println("Error executing SSH command:", err)
72 | os.Exit(1)
73 | }
74 | },
75 | }
76 |
77 | var extraOptions string
78 |
79 | func init() {
80 | connectCmd.Flags().StringVarP(&extraOptions, "options", "o", "", "openssh options passed to ssh command")
81 | rootCmd.AddCommand(connectCmd)
82 |
83 | // Here you will define your flags and configuration settings.
84 |
85 | // Cobra supports Persistent Flags which will work for this command
86 | // and all subcommands, e.g.:
87 | // connectCmd.PersistentFlags().String("foo", "", "A help for foo")
88 |
89 | // Cobra supports local flags which will only run when this command
90 | // is called directly, e.g.:
91 | // connectCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
92 | }
93 |
--------------------------------------------------------------------------------
/cmd/edit.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package cmd
15 |
16 | import (
17 | "bufio"
18 | "fmt"
19 | "os"
20 | "os/exec"
21 | "strings"
22 |
23 | "github.com/spf13/cobra"
24 | )
25 |
26 | var editCmd = &cobra.Command{
27 | Use: "edit",
28 | Short: "Edit sshabu config file",
29 | Long: `Edit the sshabu configuration file with editor.
30 | If no editor command found, ask you to choose between vim and nano.
31 |
32 | After editing you will be promted if you'd like to use 'sshabu apply'`,
33 | Run: func(cmd *cobra.Command, args []string) {
34 | editFile(cfgFile)
35 | },
36 | }
37 |
38 | func editFile(filePath string) {
39 | cmd := exec.Command("editor", filePath)
40 | cmd.Stdin = os.Stdin
41 | cmd.Stdout = os.Stdout
42 | cmd.Stderr = os.Stderr
43 | err := cmd.Run()
44 | if err != nil {
45 |
46 | fmt.Println("⸫ Using config file:", cfgFile)
47 |
48 | editor := ""
49 | fmt.Println("Editor is not installed.")
50 | fmt.Println("Choose an editor [nano/vim or press Enter]: ")
51 | reader := bufio.NewReader(os.Stdin)
52 | choice, _ := reader.ReadString('\n')
53 | choice = strings.TrimSpace(choice)
54 | switch choice {
55 | case "nano":
56 | editor = "nano"
57 | case "vim":
58 | editor = "vim"
59 | default:
60 | fmt.Println("Vim is the right choice!")
61 | editor = "vim"
62 | }
63 | cmd := exec.Command(editor, filePath)
64 | cmd.Stdin = os.Stdin
65 | cmd.Stdout = os.Stdout
66 | cmd.Stderr = os.Stderr
67 | err := cmd.Run()
68 | if err != nil {
69 | fmt.Printf("Failed to open editor: %v\n", err)
70 | return
71 | }
72 | }
73 |
74 |
75 | reader := bufio.NewReader(os.Stdin)
76 | fmt.Print("Would you like sshabu to apply changes? [y/n]: ")
77 | text, _ := reader.ReadString('\n')
78 | text = strings.TrimSpace(text)
79 |
80 |
81 | if strings.ToLower(text) == "y" {
82 | if err := RunApply([]string{}); err != nil {
83 | cobra.CheckErr(err)
84 | }
85 | } else {
86 | fmt.Println("Ok.(╥﹏╥)")
87 | fmt.Println("Changes was not applied.")
88 | }
89 | }
90 |
91 |
92 | func init() {
93 | rootCmd.AddCommand(editCmd)
94 | }
95 |
--------------------------------------------------------------------------------
/cmd/init.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | // "fmt"
19 | // "log"
20 | _ "embed"
21 | "fmt"
22 | "os"
23 | sshabu "sshabu/pkg"
24 |
25 | "github.com/spf13/cobra"
26 | )
27 |
28 | // initCmd represents the init command
29 | var initCmd = &cobra.Command{
30 | PersistentPreRun: func(cmd *cobra.Command, args []string){
31 |
32 | },
33 | Use: "init",
34 | Short: "Create default directories",
35 | Long: `Init command search for $HOME/.sshabu/ directory.
36 | If no directory found, init will create it and create default $HOME/.sshabu/sshabu.yaml config.`,
37 | Run: func(cmd *cobra.Command, args []string) {
38 | home, err := os.UserHomeDir()
39 | cobra.CheckErr(err)
40 | if _, err := os.Stat(home+"/.sshabu/"); os.IsNotExist(err) {
41 | fmt.Println("Creating base paths")
42 | err = os.MkdirAll(home+"/.sshabu/", 0750)
43 | cobra.CheckErr(err)
44 | err = os.WriteFile(home+"/.sshabu/sshabu.yaml", []byte(sshabu.ConfigExample()), 0660)
45 | fmt.Println("Success ʕ♥ᴥ♥ʔ")
46 | cobra.CheckErr(err)
47 | } else {
48 | fmt.Println("Base sshabu path already exists")
49 | fmt.Println("Doing nothing ಠ_ಠ")
50 | }
51 |
52 | },
53 | }
54 |
55 | func init() {
56 | rootCmd.AddCommand(initCmd)
57 |
58 | // Here you will define your flags and configuration settings.
59 |
60 | // Cobra supports Persistent Flags which will work for this command
61 | // and all subcommands, e.g.:
62 | // initCmd.PersistentFlags().String("foo", "", "A help for foo")
63 |
64 | // Cobra supports local flags which will only run when this command
65 | // is called directly, e.g.:
66 | // initCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
67 | }
68 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "fmt"
19 | "os"
20 | "path/filepath"
21 |
22 | "github.com/spf13/cobra"
23 | "github.com/spf13/viper"
24 | // "sshabu/pkg"
25 | )
26 |
27 | var cfgFile string
28 | var opensshTmpFile string
29 | var opensshDestconfigFile string
30 |
31 | // rootCmd represents the base command when called without any subcommands
32 | var rootCmd = &cobra.Command{
33 | PersistentPreRun: func(cmd *cobra.Command, args []string){
34 | if cmd.Name() == "completion"{
35 | return
36 | }
37 | initConfig()
38 | },
39 | Use: "sshabu",
40 | Version: "0.0.1-alpha",
41 | Short: "Is a robust SSH client management tool",
42 | Long: `is a robust SSH client management tool designed to streamline the process of connecting to multiple servers effortlessly.
43 | This tool leverages OpenSSH and offers a user-friendly interface to enhance the overall SSH experience.
44 | With Sshabu, managing SSH configurations becomes more intuitive, allowing users to organize and connect to their servers efficiently.
45 |
46 | Sshabu works with sshabu.yaml and openssh.config file.
47 | openssh.config will be created next to sshabu.yaml
48 |
49 | sshabu.yaml location - $HOME (user's home dir)
50 | `,
51 | // Uncomment the following line if your bare application
52 | // has an action associated with it:
53 | // Run: func(cmd *cobra.Command, args []string) { },
54 | }
55 |
56 |
57 | // Execute adds all child commands to the root command and sets flags appropriately.
58 | // This is called by main.main(). It only needs to happen once to the rootCmd.
59 | func Execute() {
60 | err := rootCmd.Execute()
61 | if err != nil {
62 | os.Exit(1)
63 | }
64 | }
65 |
66 | func init() {
67 | // cobra.OnInitialize(initConfig)
68 |
69 | // Here you will define your flags and configuration settings.
70 | // Cobra supports persistent flags, which, if defined here,
71 | // will be global for your application.
72 |
73 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "manully override config file path")
74 | }
75 |
76 | // initConfig reads in config file and ENV variables if set.
77 | func initConfig() {
78 | if cfgFile != "" {
79 | viper.SetConfigFile(cfgFile)
80 | } else {
81 | home, err := os.UserHomeDir()
82 | cobra.CheckErr(err)
83 |
84 | viper.SetConfigType("yaml")
85 | viper.SetConfigName("sshabu")
86 | // viper.AddConfigPath("$PWD")
87 | viper.AddConfigPath(home+"/.sshabu")
88 | }
89 |
90 | if err := viper.ReadInConfig(); err == nil {
91 | cfgFile = viper.ConfigFileUsed()
92 | cfgPath := filepath.Dir(cfgFile)
93 | opensshTmpFile = cfgPath+"/openssh.tmp"
94 | opensshDestconfigFile = cfgPath+"/openssh.config"
95 |
96 | os.OpenFile(opensshTmpFile, os.O_RDONLY|os.O_CREATE, 0666)
97 | os.OpenFile(opensshDestconfigFile, os.O_RDONLY|os.O_CREATE, 0666)
98 | } else {
99 | fmt.Println("(╯°□°)╯︵ ɹoɹɹƎ")
100 | cfgFile, _ = filepath.Abs(viper.ConfigFileUsed())
101 | fmt.Printf("\n%s\n└─ %s",cfgFile,err)
102 | os.Exit(1)
103 | }
104 | }
105 |
106 |
107 | func SetVersionInfo(version, commit, date string) {
108 | rootCmd.Version = fmt.Sprintf("%s \nBuilt on %s from Git SHA %s)", version, date, commit)
109 | }
--------------------------------------------------------------------------------
/docs/license.tpl:
--------------------------------------------------------------------------------
1 | {{ range . }}
2 | ## {{ .Name }}
3 |
4 | * Name: {{ .Name }}
5 | * Version: {{ .Version }}
6 | * License: [{{ .LicenseName }}]({{ .LicenseURL }})
7 |
8 | ```
9 | {{ .LicenseText }}
10 | ```
11 | {{ end }}
12 |
--------------------------------------------------------------------------------
/docs/sshabu-quick.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ratsky-oss/sshabu/7998f35a8e6984fa27c4a58d938f2d06ee9b2749/docs/sshabu-quick.gif
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module sshabu
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/spf13/cobra v1.7.0
7 | github.com/spf13/viper v1.16.0
8 | )
9 |
10 | require (
11 | github.com/Masterminds/goutils v1.1.1 // indirect
12 | github.com/Masterminds/semver/v3 v3.2.0 // indirect
13 | github.com/google/uuid v1.1.2 // indirect
14 | github.com/huandu/xstrings v1.3.3 // indirect
15 | github.com/imdario/mergo v0.3.11 // indirect
16 | github.com/mitchellh/copystructure v1.0.0 // indirect
17 | github.com/mitchellh/reflectwalk v1.0.0 // indirect
18 | github.com/shopspring/decimal v1.2.0 // indirect
19 | golang.org/x/crypto v0.9.0 // indirect
20 | )
21 |
22 | require (
23 | github.com/Masterminds/sprig/v3 v3.2.3
24 | github.com/fsnotify/fsnotify v1.6.0 // indirect
25 | github.com/hashicorp/hcl v1.0.0 // indirect
26 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
27 | github.com/magiconair/properties v1.8.7 // indirect
28 | github.com/mitchellh/mapstructure v1.5.0 // indirect
29 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect
30 | github.com/spf13/afero v1.9.5 // indirect
31 | github.com/spf13/cast v1.5.1 // indirect
32 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
33 | github.com/spf13/pflag v1.0.5 // indirect
34 | github.com/subosito/gotenv v1.4.2 // indirect
35 | golang.org/x/sys v0.8.0 // indirect
36 | golang.org/x/text v0.9.0 // indirect
37 | gopkg.in/ini.v1 v1.67.0 // indirect
38 | gopkg.in/yaml.v3 v3.0.1 // indirect
39 | )
40 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
41 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
42 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
43 | github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
44 | github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
45 | github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
46 | github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
47 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
48 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
49 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
50 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
51 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
52 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
53 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
54 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
55 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
56 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
57 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
58 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
59 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
60 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
61 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
62 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
63 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
64 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
65 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
66 | github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
67 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
68 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
69 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
70 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
71 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
72 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
73 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
74 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
75 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
76 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
77 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
78 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
79 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
80 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
81 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
82 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
83 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
84 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
85 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
86 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
87 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
88 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
89 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
90 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
91 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
92 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
93 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
94 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
95 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
96 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
97 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
98 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
99 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
100 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
101 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
102 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
103 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
104 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
105 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
106 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
107 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
108 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
109 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
110 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
111 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
112 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
113 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
114 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
115 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
116 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
117 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
118 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
119 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
120 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
121 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
122 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
123 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
124 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
125 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
126 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
127 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
128 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
129 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
130 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
131 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
132 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
133 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
134 | github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
135 | github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
136 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
137 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
138 | github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
139 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
140 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
141 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
142 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
143 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
144 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
145 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
146 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
147 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
148 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
149 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
150 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
151 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
152 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
153 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
154 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
155 | github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
156 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
157 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
158 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
159 | github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
160 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
161 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
162 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
163 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
164 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
165 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
166 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
167 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
168 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
169 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
170 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
171 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
172 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
173 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
174 | github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
175 | github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
176 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
177 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
178 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
179 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
180 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
181 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
182 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
183 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
184 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
185 | github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
186 | github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
187 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
188 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
189 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
190 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
191 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
192 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
193 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
194 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
195 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
196 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
197 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
198 | github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
199 | github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
200 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
201 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
202 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
203 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
204 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
205 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
206 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
207 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
208 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
209 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
210 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
211 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
212 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
213 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
214 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
215 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
216 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
217 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
218 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
219 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
220 | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
221 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
222 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
223 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
224 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
225 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
226 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
227 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
228 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
229 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
230 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
231 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
232 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
233 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
234 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
235 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
236 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
237 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
238 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
239 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
240 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
241 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
242 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
243 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
244 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
245 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
246 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
247 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
248 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
249 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
250 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
251 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
252 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
253 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
254 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
255 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
256 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
257 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
258 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
259 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
260 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
261 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
262 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
263 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
264 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
265 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
266 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
267 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
268 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
269 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
270 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
271 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
272 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
273 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
274 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
275 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
276 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
277 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
278 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
279 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
280 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
281 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
282 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
283 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
284 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
285 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
286 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
287 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
288 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
289 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
290 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
291 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
292 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
293 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
294 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
295 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
296 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
297 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
298 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
299 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
300 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
301 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
302 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
303 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
304 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
305 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
306 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
307 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
308 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
309 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
310 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
311 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
312 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
313 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
314 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
315 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
316 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
317 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
318 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
319 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
320 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
321 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
322 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
323 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
324 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
325 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
326 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
327 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
328 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
329 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
330 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
331 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
332 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
333 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
334 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
335 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
336 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
337 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
338 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
339 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
340 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
341 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
342 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
343 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
344 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
345 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
346 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
347 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
348 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
349 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
350 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
351 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
352 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
353 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
354 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
355 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
356 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
357 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
358 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
359 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
360 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
361 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
362 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
363 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
364 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
365 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
366 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
367 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
368 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
369 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
370 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
371 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
372 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
373 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
374 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
375 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
376 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
377 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
378 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
379 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
380 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
381 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
382 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
383 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
384 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
385 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
386 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
387 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
388 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
389 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
390 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
391 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
392 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
393 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
394 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
395 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
396 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
397 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
398 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
399 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
400 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
401 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
402 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
403 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
404 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
405 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
406 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
407 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
408 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
409 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
410 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
411 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
412 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
413 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
414 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
415 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
416 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
417 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
418 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
419 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
420 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
421 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
422 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
423 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
424 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
425 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
426 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
427 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
428 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
429 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
430 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
431 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
432 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
433 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
434 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
435 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
436 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
437 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
438 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
439 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
440 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
441 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
442 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
443 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
444 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
445 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
446 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
447 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
448 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
449 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
450 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
451 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
452 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
453 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
454 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
455 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
456 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
457 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
458 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
459 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
460 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
461 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
462 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
463 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
464 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
465 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
466 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
467 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
468 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
469 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
470 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
471 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
472 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
473 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
474 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
475 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
476 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
477 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
478 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
479 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
480 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
481 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
482 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
483 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
484 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
485 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
486 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
487 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
488 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
489 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
490 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
491 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
492 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
493 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
494 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
495 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
496 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
497 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
498 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
499 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
500 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
501 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
502 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
503 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
504 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
505 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
506 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
507 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
508 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
509 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
510 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
511 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
512 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
513 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
514 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
515 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
516 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
517 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
518 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
519 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
520 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
521 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
522 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
523 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
524 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
525 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
526 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
527 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
528 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
529 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
530 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2023 alvtsky github.com/Ra-sky
3 | */
4 | package main
5 |
6 | import (
7 | "sshabu/cmd"
8 | )
9 |
10 | var (
11 | version = "dev"
12 | commit = "none"
13 | date = "unknown"
14 | )
15 |
16 | func main() {
17 | cmd.SetVersionInfo(version, commit, date)
18 | cmd.Execute()
19 | }
--------------------------------------------------------------------------------
/pkg/checker.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package sshabu
16 |
17 | import (
18 | "bufio"
19 | "fmt"
20 | "io"
21 | "os"
22 | "os/exec"
23 | "strings"
24 | )
25 |
26 | func OpensshCheck(openssh_cfg string) error {
27 | fmt.Println("Verifing result...")
28 | vcmd := exec.Command("bash","-c","ssh -GTF " + openssh_cfg + " test")
29 | vcmd.Stderr = os.Stderr
30 | vcmd.Stdin = nil
31 | if err := vcmd.Run(); err != nil{
32 | return err
33 | }
34 | fmt.Println("Seems legit to me")
35 | return nil
36 | }
37 |
38 | func DestinationHosts(r io.Reader) ([]string, error) {
39 | scanner := bufio.NewScanner(r)
40 |
41 | // Slice to store values after "Host "
42 | hostValues := []string{}
43 |
44 | for scanner.Scan() {
45 | line := scanner.Text()
46 |
47 | // Check if the line starts with "Host " and doesn't contain "*" or "!"
48 | if strings.HasPrefix(line, "Host ") && !strings.Contains(line, "*") && !strings.Contains(line, "!") {
49 | hostValue := strings.TrimPrefix(line, "Host ")
50 |
51 | // Split hostValue by spaces and add the resulting entities to hostValues
52 | entities := strings.Fields(hostValue)
53 | hostValues = append(hostValues, entities...)
54 | }
55 | }
56 |
57 | if err := scanner.Err(); err != nil {
58 | return nil, err
59 | }
60 |
61 | return hostValues, nil
62 | }
63 |
64 | func AskForConfirmation() bool {
65 | var response string
66 | _, err := fmt.Scanln(&response)
67 | if err != nil {
68 | fmt.Println("Please enter 'yes' or 'no'.")
69 | return false
70 | }
71 | response = strings.ToLower(response)
72 | switch response {
73 | case "yes", "y":
74 | return true
75 | case "no", "n":
76 | return false
77 | default:
78 | fmt.Println("Please enter 'yes' or 'no'.")
79 | return false
80 | }
81 | }
--------------------------------------------------------------------------------
/pkg/checker_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package sshabu
16 |
17 | import (
18 | "bytes"
19 | // "io"
20 | "os"
21 | "os/exec"
22 | "path/filepath"
23 | "reflect"
24 | "testing"
25 |
26 | "github.com/spf13/viper"
27 | )
28 |
29 | var execCommand = exec.Command
30 |
31 | func render(t *testing.T) string {
32 | currentDir, err := os.Getwd()
33 | if err != nil {
34 | t.Fatal(err)
35 | }
36 |
37 | // Create the path to sshabu_example.yaml
38 | yamlFilePath := filepath.Join(currentDir, "sshabu_example.yaml")
39 | configFilePath := filepath.Join(currentDir, "sshabu_example.config")
40 | // Set up Viper with the YAML file
41 | viper.SetConfigFile(yamlFilePath)
42 | err = viper.ReadInConfig()
43 | if err != nil {
44 | t.Fatal(err)
45 | }
46 |
47 | // Unmarshal YAML into Shabu
48 | var shabu Shabu
49 | err = viper.UnmarshalExact(&shabu)
50 | if err != nil {
51 | t.Fatal(err)
52 | }
53 |
54 | // Call Boil
55 | err = shabu.Boil()
56 | if err != nil {
57 | t.Fatal(err)
58 | }
59 |
60 | // Create a buffer for RenderTemplate
61 | buf := new(bytes.Buffer)
62 |
63 | // Render the template
64 | err = RenderTemplate(shabu, buf)
65 | if err != nil {
66 | t.Fatal(err)
67 | }
68 | os.WriteFile(configFilePath, buf.Bytes(), 0600)
69 | return configFilePath
70 | }
71 |
72 | // TODO: Do not use sshabu_example.yaml rendering
73 | func TestOpensshCheck(t *testing.T) {
74 | tests := []struct {
75 | name string
76 | cmd *exec.Cmd
77 | wantErr bool
78 | }{
79 | {
80 | name: "Test with successful SSH check",
81 | cmd: exec.Command("ssh", "-GTF", "test"),
82 | // For a real SSH check, you would set cmd: exec.Command("ssh", "-GTF", "fake_config", "test"),
83 | wantErr: false,
84 | },
85 | // Add more test cases as needed
86 | }
87 |
88 | for _, tt := range tests {
89 | t.Run(tt.name, func(t *testing.T) {
90 | execCommand = func(name string, arg ...string) *exec.Cmd {
91 | return tt.cmd
92 | }
93 | defer func() { execCommand = exec.Command }()
94 | path := render(t)
95 | if err := OpensshCheck(path); (err != nil) != tt.wantErr {
96 | t.Errorf("OpensshCheck() error = %v, wantErr %v", err, tt.wantErr)
97 | }
98 | os.Remove(path)
99 | })
100 | }
101 | }
102 |
103 | // TODO: Do not use sshabu_example.yaml rendering
104 | func TestDestinationHosts(t *testing.T) {
105 | // type args struct {
106 | // r io.Reader
107 | // }
108 | tests := []struct {
109 | name string
110 | // args args
111 | want []string
112 | wantErr bool
113 | }{
114 | {
115 | name: "CRAP",
116 | want: []string{
117 | "project1-test",
118 | "project2-dev",
119 | "home-gitlab",
120 | },
121 | },
122 | }
123 | for _, tt := range tests {
124 | t.Run(tt.name, func(t *testing.T) {
125 | path := render(t)
126 |
127 | file, _ := os.Open(path)
128 |
129 | defer file.Close()
130 |
131 |
132 | got, err := DestinationHosts(file)
133 | if (err != nil) != tt.wantErr {
134 | t.Errorf("DestinationHosts() error = %v, wantErr %v", err, tt.wantErr)
135 | return
136 | }
137 | if !reflect.DeepEqual(got, tt.want) {
138 | t.Errorf("DestinationHosts() = %v, want %v", got, tt.want)
139 | }
140 | os.Remove(path)
141 | })
142 | }
143 | }
144 |
145 | func TestAskForConfirmation(t *testing.T) {
146 | tests := []struct {
147 | name string
148 | input string
149 | want bool
150 | }{
151 | {
152 | name: "Valid input 'yes'",
153 | input: "yes\n",
154 | want: true,
155 | },
156 | {
157 | name: "Valid input 'no'",
158 | input: "no\n",
159 | want: false,
160 | },
161 | {
162 | name: "Invalid input 'invalid'",
163 | input: "invalid\n",
164 | want: false,
165 | },
166 | {
167 | name: "Invalid input 'invalid'",
168 | input: "3\n",
169 | want: false,
170 | },
171 | }
172 |
173 | for _, tt := range tests {
174 | t.Run(tt.name, func(t *testing.T) {
175 | // Redirect stdin for testing
176 | oldStdin := os.Stdin
177 | defer func() { os.Stdin = oldStdin }()
178 | r, w, _ := os.Pipe()
179 | os.Stdin = r
180 | defer w.Close()
181 |
182 | // Write input to stdin
183 | _, _ = w.WriteString(tt.input)
184 |
185 | got := AskForConfirmation()
186 |
187 | if got != tt.want {
188 | t.Errorf("AskForConfirmation() = %v, want %v", got, tt.want)
189 | }
190 | })
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/pkg/compare/compare.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package compare
15 |
16 | import (
17 | "os"
18 | "bufio"
19 | "fmt"
20 | )
21 |
22 | const (
23 | Reset = "\033[0m"
24 | Red = "\033[31m"
25 | Green = "\033[32m"
26 | Yellow = "\033[33m"
27 | Blue = "\033[34m"
28 | Purple = "\033[35m"
29 | Cyan = "\033[36m"
30 | Gray = "\033[37m"
31 | White = "\033[97m"
32 |
33 | )
34 |
35 | type Difference struct {
36 | lineNumber int
37 | line string
38 | Added bool
39 | }
40 |
41 | type Bites struct {
42 | length int
43 | Content []string
44 | }
45 |
46 | func (bites *Bites) TakeBites(path string) {
47 | var lineArray []string
48 |
49 | file, err := os.Open(path)
50 | check(err)
51 | defer file.Close()
52 |
53 | scanner := bufio.NewScanner(file)
54 |
55 | for scanner.Scan() {
56 | line := scanner.Text()
57 | lineArray = append(lineArray, line)
58 | }
59 |
60 | bites.Content = lineArray
61 | bites.length = len(lineArray)
62 | }
63 |
64 | func TransformDifferencesToReadableFormat(differences []Difference, firstBites Bites, secondBites Bites) []string {
65 | var result []string
66 |
67 | // Определение максимальной длины номера строки для выравнивания
68 | maxLineNum := max(firstBites.length, len(secondBites.Content))
69 | maxLineNumLen := len(fmt.Sprintf("%d", maxLineNum))
70 |
71 | // Форматирование строки с учетом выравнивания номера строки
72 | // lineFormat := fmt.Sprintf("%%%dd: %%s%%s%%s", maxLineNumLen)
73 |
74 | for index, line := range secondBites.Content {
75 | color := Reset
76 | resultStr := ""
77 | resultStr = fmt.Sprintf("%*d: %s%s%s", maxLineNumLen, index+1, color, line, Reset)
78 | for _, diff := range differences {
79 | if diff.lineNumber == index+1 {
80 | if diff.Added {
81 | color = Green
82 | resultStr = fmt.Sprintf("%*d:%s + %s%s", maxLineNumLen, index+1, color, line, Reset)
83 | } else {
84 | color = Red
85 | resultStr = fmt.Sprintf("%*d:%s - %s\n %*s%s+ %s%s", maxLineNumLen, index+1, color, diff.line ,maxLineNumLen,"",Green, line, Reset)
86 | }
87 | break
88 | }
89 | }
90 | result = append(result, resultStr)
91 | }
92 |
93 | if len(result) < firstBites.length{
94 | for _, diff := range differences {
95 | if diff.lineNumber > len(result){
96 | color := Red
97 | resultStr := fmt.Sprintf("%*d:%s - %s%s", maxLineNumLen, diff.lineNumber, color, diff.line, Reset)
98 | result = append(result, resultStr)
99 | }
100 | }
101 | }
102 |
103 | return result
104 | }
105 |
106 | func DiffBites(bites1, bites2 Bites) []Difference{
107 | var differences []Difference
108 | maxLen := 0
109 | for _, line := range bites1.Content {
110 | if len(line) > maxLen {
111 | maxLen = len(line)
112 | }
113 | }
114 |
115 | lcsMatrix := make([][]int, len(bites1.Content)+1)
116 | for i := range lcsMatrix {
117 | lcsMatrix[i] = make([]int, len(bites2.Content)+1)
118 | }
119 |
120 | for i := 1; i <= len(bites1.Content); i++ {
121 | for j := 1; j <= len(bites2.Content); j++ {
122 | if bites1.Content[i-1] == bites2.Content[j-1] {
123 | lcsMatrix[i][j] = lcsMatrix[i-1][j-1] + 1
124 | } else {
125 | lcsMatrix[i][j] = max(lcsMatrix[i-1][j], lcsMatrix[i][j-1])
126 | }
127 | }
128 | }
129 |
130 | i, j := len(bites1.Content), len(bites2.Content)
131 | for i > 0 || j > 0 {
132 | if i > 0 && j > 0 && bites1.Content[i-1] == bites2.Content[j-1] {
133 | i--
134 | j--
135 | } else if j > 0 && (i == 0 || lcsMatrix[i][j-1] >= lcsMatrix[i-1][j]) {
136 | differences = append([]Difference{{lineNumber: j, line: bites2.Content[j-1], Added: true}}, differences...)
137 | j--
138 | } else if i > 0 && (j == 0 || lcsMatrix[i][j-1] < lcsMatrix[i-1][j]) {
139 | differences = append([]Difference{{lineNumber: i, line: bites1.Content[i-1], Added: false}}, differences...)
140 | i--
141 | }
142 | }
143 |
144 | return differences
145 | }
146 |
147 | func check(e error) {
148 | if e != nil {
149 | panic(e)
150 | }
151 | }
152 |
153 | func max(a, b int) int {
154 | if a > b {
155 | return a
156 | }
157 | return b
158 | }
159 |
160 |
161 |
162 |
163 |
164 |
--------------------------------------------------------------------------------
/pkg/compare/compare_test.go:
--------------------------------------------------------------------------------
1 | package compare
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | "fmt"
7 | )
8 |
9 | func Test_transformDifferencesToReadableFormat(t *testing.T) {
10 | type args struct {
11 | differences []Difference
12 | firstBites Bites
13 | secondBites Bites
14 | }
15 | tests := []struct {
16 | name string
17 | args args
18 | want []string
19 | }{
20 | {
21 | name: "No differences",
22 | args: args{
23 | differences: []Difference{},
24 | firstBites: Bites{Content: []string{"A"}},
25 | secondBites: Bites{Content: []string{"A"}},
26 | },
27 | want: []string{ fmt.Sprintf("%d: %s%s%s", 1, White, "A", White)},
28 | },
29 | {
30 | name: "Addition in the second file",
31 | args: args{
32 | differences: []Difference{{lineNumber: 2, line: "B", Added: true}},
33 | firstBites: Bites{Content: []string{"A"}},
34 | secondBites: Bites{Content: []string{"A","B"}},
35 | },
36 | want: []string{
37 | fmt.Sprintf("%d: %s%s%s", 1, White, "A", White),
38 | fmt.Sprintf("%d: %s%s%s", 2, Green, "B", White),
39 | },
40 | },
41 | }
42 | for _, tt := range tests {
43 | t.Run(tt.name, func(t *testing.T) {
44 | if got := TransformDifferencesToReadableFormat(tt.args.differences, tt.args.firstBites, tt.args.secondBites); !reflect.DeepEqual(got, tt.want) {
45 | t.Errorf("transformDifferencesToReadableFormat() = %v, want %v", got, tt.want)
46 | }
47 | })
48 | }
49 | }
50 |
51 | func Test_diffBites(t *testing.T) {
52 | type args struct {
53 | bites1 Bites
54 | bites2 Bites
55 | }
56 | tests := []struct {
57 | name string
58 | args args
59 | want []Difference
60 | }{
61 | {
62 | name: "Add one line",
63 | args: args{
64 | bites1: Bites{Content: []string{"A","B", "C"}},
65 | bites2: Bites{Content: []string{"A","B", "C", "D"}},
66 | },
67 | want: []Difference{{lineNumber: 4, line: "D", Added: true}},
68 | },
69 | {
70 | name: "Del one line",
71 | args: args{
72 | bites1: Bites{Content: []string{"A","B", "C"}},
73 | bites2: Bites{Content: []string{"A","B"}},
74 | },
75 | want: []Difference{{lineNumber: 3, line: "C", Added: false}},
76 | },
77 | {
78 | name: "Multy edit",
79 | args: args{
80 | bites1: Bites{Content: []string{"B","E","D","E"}},
81 | bites2: Bites{Content: []string{"A","B","D","E","D","E","C"}},
82 | },
83 | want: []Difference{ {lineNumber: 1, line: "A", Added: true},
84 | {lineNumber: 3, line: "D", Added: true},
85 | {lineNumber: 7, line: "C", Added: true},
86 | },
87 | },
88 | }
89 | for _, tt := range tests {
90 | t.Run(tt.name, func(t *testing.T) {
91 | if got := DiffBites(tt.args.bites1, tt.args.bites2); !reflect.DeepEqual(got, tt.want) {
92 | t.Errorf("diffBites() = %v, want %v", got, tt.want)
93 | }
94 | })
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/pkg/openssh_conf.gtpl:
--------------------------------------------------------------------------------
1 | {{- /*
2 | Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.*/ -}}
15 | # -----------------------
16 | # RATSKY SSHABU
17 | {{ with .Options }}
18 | {{ include "option" . }}
19 | {{- end -}}
20 | {{- range .Hosts -}}
21 | {{ include "host" . }}
22 | {{- end -}}
23 | {{- range .Groups -}}
24 | {{ include "group" . }}
25 | {{- end -}}
26 | {{- /*----------------------------------------------*/ -}}
27 | {{ define "group" }}
28 | {{- range .Hosts -}}
29 | {{ include "host" . }}
30 | {{- end -}}
31 | {{- range .Subgroups -}}
32 | {{ include "group" . }}
33 | {{- end -}}
34 | {{- end -}}
35 | {{- /*----------------------------------------------*/ -}}
36 | {{ define "host" }}
37 | {{- if .Options.Host }}
38 | Host {{ .Options.Host }}
39 | {{- else }}
40 | Host {{ .Name }}
41 | {{- end -}}
42 | {{- with .Options }}
43 | {{ include "option" . | indent 4 }}
44 | {{- end -}}
45 | {{- end -}}
46 | {{- /*----------------------------------------------*/ -}}
47 | {{ define "option" -}}
48 | {{- if .AddKeysToAgent }}AddKeysToAgent {{ .AddKeysToAgent }}
49 | {{ end }}
50 | {{- if .AddressFamily }}AddressFamily {{ .AddressFamily }}
51 | {{ end }}
52 | {{- if .BatchMode }}BatchMode {{ .BatchMode }}
53 | {{ end }}
54 | {{- if .BindAddress }}BindAddress {{ .BindAddress }}
55 | {{ end }}
56 | {{- if .CanonicalDomains }}CanonicalDomains {{ .CanonicalDomains }}
57 | {{ end }}
58 | {{- if .CanonicalizeFallbackLocal }}CanonicalizeFallbackLocal {{ .CanonicalizeFallbackLocal }}
59 | {{ end }}
60 | {{- if .CanonicalizeHostname }}CanonicalizeHostname {{ .CanonicalizeHostname }}
61 | {{ end }}
62 | {{- if .CanonicalizeMaxDots }}CanonicalizeMaxDots {{ .CanonicalizeMaxDots }}
63 | {{ end }}
64 | {{- if .CanonicalizePermittedCNAMEs }}CanonicalizePermittedCNAMEs {{ .CanonicalizePermittedCNAMEs }}
65 | {{ end }}
66 | {{- if .CASignatureAlgorithms }}CASignatureAlgorithms {{ .CASignatureAlgorithms }}
67 | {{ end }}
68 | {{- if .CertificateFile }}CertificateFile {{ .CertificateFile }}
69 | {{ end }}
70 | {{- if .CheckHostIP }}CheckHostIP {{ .CheckHostIP }}
71 | {{ end }}
72 | {{- if .Ciphers }}Ciphers {{ .Ciphers }}
73 | {{ end }}
74 | {{- if .ClearAllForwardings }}ClearAllForwardings {{ .ClearAllForwardings }}
75 | {{ end }}
76 | {{- if .Compression }}Compression {{ .Compression }}
77 | {{ end }}
78 | {{- if .ConnectionAttempts }}ConnectionAttempts {{ .ConnectionAttempts }}
79 | {{ end }}
80 | {{- if .ConnectTimeout }}ConnectTimeout {{ .ConnectTimeout }}
81 | {{ end }}
82 | {{- if .ControlMaster }}ControlMaster {{ .ControlMaster }}
83 | {{ end }}
84 | {{- if .ControlPath }}ControlPath {{ .ControlPath }}
85 | {{ end }}
86 | {{- if .ControlPersist }}ControlPersist {{ .ControlPersist }}
87 | {{ end }}
88 | {{- if .DynamicForward }}DynamicForward {{ .DynamicForward }}
89 | {{ end }}
90 | {{- if .EscapeChar }}EscapeChar {{ .EscapeChar }}
91 | {{ end }}
92 | {{- if .ExitOnForwardFailure }}ExitOnForwardFailure {{ .ExitOnForwardFailure }}
93 | {{ end }}
94 | {{- if .FingerprintHash }}FingerprintHash {{ .FingerprintHash }}
95 | {{ end }}
96 | {{- if .ForkAfterAuthentication }}ForkAfterAuthentication {{ .ForkAfterAuthentication }}
97 | {{ end }}
98 | {{- if .ForwardAgent }}ForwardAgent {{ .ForwardAgent }}
99 | {{ end }}
100 | {{- if .ForwardX11 }}ForwardX11 {{ .ForwardX11 }}
101 | {{ end }}
102 | {{- if .ForwardX11Timeout }}ForwardX11Timeout {{ .ForwardX11Timeout }}
103 | {{ end }}
104 | {{- if .ForwardX11Trusted }}ForwardX11Trusted {{ .ForwardX11Trusted }}
105 | {{ end }}
106 | {{- if .GatewayPorts }}GatewayPorts {{ .GatewayPorts }}
107 | {{ end }}
108 | {{- if .GlobalKnownHostsFile }}GlobalKnownHostsFile {{ .GlobalKnownHostsFile }}
109 | {{ end }}
110 | {{- if .GSSAPIAuthentication }}GSSAPIAuthentication {{ .GSSAPIAuthentication }}
111 | {{ end }}
112 | {{- if .GSSAPIDelegateCredentials }}GSSAPIDelegateCredentials {{ .GSSAPIDelegateCredentials }}
113 | {{ end }}
114 | {{- if .HashKnownHosts }}HashKnownHosts {{ .HashKnownHosts }}
115 | {{ end }}
116 | {{- if .HostbasedAcceptedAlgorithms }}HostbasedAcceptedAlgorithms {{ .HostbasedAcceptedAlgorithms }}
117 | {{ end }}
118 | {{- if .HostbasedAuthentication }}HostbasedAuthentication {{ .HostbasedAuthentication }}
119 | {{ end }}
120 | {{- if .HostKeyAlgorithms }}HostKeyAlgorithms {{ .HostKeyAlgorithms }}
121 | {{ end }}
122 | {{- if .HostKeyAlias }}HostKeyAlias {{ .HostKeyAlias }}
123 | {{ end }}
124 | {{- if .Hostname }}Hostname {{ .Hostname }}
125 | {{ end }}
126 | {{- if .IdentitiesOnly }}IdentitiesOnly {{ .IdentitiesOnly }}
127 | {{ end }}
128 | {{- if .IdentityAgent }}IdentityAgent {{ .IdentityAgent }}
129 | {{ end }}
130 | {{- if .IdentityFile }}IdentityFile {{ .IdentityFile }}
131 | {{ end }}
132 | {{- if .IPQoS }}IPQoS {{ .IPQoS }}
133 | {{ end }}
134 | {{- if .KbdInteractiveDevices }}KbdInteractiveDevices {{ .KbdInteractiveDevices }}
135 | {{ end }}
136 | {{- if .KexAlgorithms }}KexAlgorithms {{ .KexAlgorithms }}
137 | {{ end }}
138 | {{- if .KnownHostsCommand }}KnownHostsCommand {{ .KnownHostsCommand }}
139 | {{ end }}
140 | {{- if .LocalCommand }}LocalCommand {{ .LocalCommand }}
141 | {{ end }}
142 | {{- if .LocalForward }}LocalForward {{ .LocalForward }}
143 | {{ end }}
144 | {{- if .LogLevel -}}LogLevel {{ .LogLevel }}
145 | {{ end }}
146 | {{- if .MACs }}MACs {{ .MACs }}
147 | {{ end }}
148 | {{- if .NoHostAuthenticationForLocalhost }}NoHostAuthenticationForLocalhost {{ .NoHostAuthenticationForLocalhost }}
149 | {{ end }}
150 | {{- if .NumberOfPasswordPrompts }}NumberOfPasswordPrompts {{ .NumberOfPasswordPrompts }}
151 | {{ end }}
152 | {{- if .PasswordAuthentication }}PasswordAuthentication {{ .PasswordAuthentication }}
153 | {{ end }}
154 | {{- if .PermitLocalCommand }}PermitLocalCommand {{ .PermitLocalCommand }}
155 | {{ end }}
156 | {{- if .PermitRemoteOpen }}PermitRemoteOpen {{ .PermitRemoteOpen }}
157 | {{ end }}
158 | {{- if .PKCS11Provider }}PKCS11Provider {{ .PKCS11Provider }}
159 | {{ end }}
160 | {{- if .Port }}Port {{ .Port }}
161 | {{ end }}
162 | {{- if .PreferredAuthentications }}PreferredAuthentications {{ .PreferredAuthentications }}
163 | {{ end }}
164 | {{- if .ProxyCommand }}ProxyCommand {{ .ProxyCommand }}
165 | {{ end }}
166 | {{- if .ProxyJump }}ProxyJump {{ .ProxyJump }}
167 | {{ end }}
168 | {{- if .ProxyUseFdpass }}ProxyUseFdpass {{ .ProxyUseFdpass }}
169 | {{ end }}
170 | {{- if .PubkeyAcceptedAlgorithms }}PubkeyAcceptedAlgorithms {{ .PubkeyAcceptedAlgorithms }}
171 | {{ end }}
172 | {{- if .PubkeyAuthentication }}PubkeyAuthentication {{ .PubkeyAuthentication }}
173 | {{ end }}
174 | {{- if .RekeyLimit }}RekeyLimit {{ .RekeyLimit }}
175 | {{ end }}
176 | {{- if .RemoteCommand }}RemoteCommand {{ .RemoteCommand }}
177 | {{ end }}
178 | {{- if .RemoteForward }}RemoteForward {{ .RemoteForward }}
179 | {{ end }}
180 | {{- if .RequestTTY }}RequestTTY {{ .RequestTTY }}
181 | {{ end }}
182 | {{- if .SendEnv }}SendEnv {{ .SendEnv }}
183 | {{ end }}
184 | {{- if .ServerAliveInterval }}ServerAliveInterval {{ .ServerAliveInterval }}
185 | {{ end }}
186 | {{- if .ServerAliveCountMax }}ServerAliveCountMax {{ .ServerAliveCountMax }}
187 | {{ end }}
188 | {{- if .SessionType }}SessionType {{ .SessionType }}
189 | {{ end }}
190 | {{- if .SetEnv }}SetEnv {{ .SetEnv }}
191 | {{ end }}
192 | {{- if .StdinNull }}StdinNull {{ .StdinNull }}
193 | {{ end }}
194 | {{- if .StreamLocalBindMask }}StreamLocalBindMask {{.StreamLocalBindMask}}
195 | {{ end }}
196 | {{- if .StreamLocalBindUnlink }}StreamLocalBindUnlink {{.StreamLocalBindUnlink}}
197 | {{ end }}
198 | {{- if .StrictHostKeyChecking }}StrictHostKeyChecking {{.StrictHostKeyChecking}}
199 | {{ end }}
200 | {{- if .TCPKeepAlive }}TCPKeepAlive {{.TCPKeepAlive}}
201 | {{ end }}
202 | {{- if .Tunnel }}Tunnel {{.Tunnel}}
203 | {{ end }}
204 | {{- if .TunnelDevice }}TunnelDevice {{.TunnelDevice}}
205 | {{ end }}
206 | {{- if .UpdateHostKeys }}UpdateHostKeys {{.UpdateHostKeys}}
207 | {{ end }}
208 | {{- if .UseKeychain }}UseKeychain {{.UseKeychain}}
209 | {{ end }}
210 | {{- if .User }}User {{.User}}
211 | {{ end }}
212 | {{- if .UserKnownHostsFile }}UserKnownHostsFile {{.UserKnownHostsFile}}
213 | {{ end }}
214 | {{- if .VerifyHostKeyDNS }}VerifyHostKeyDNS {{.VerifyHostKeyDNS}}
215 | {{ end }}
216 | {{- if .VisualHostKey }}VisualHostKey {{.VisualHostKey}}
217 | {{ end }}
218 | {{- if .XAuthLocation }}XAuthLocation {{.XAuthLocation}}
219 | {{ end }}
220 | {{- end -}}
--------------------------------------------------------------------------------
/pkg/sshabu_example.yaml:
--------------------------------------------------------------------------------
1 | # # ----------------------------------------------------------------------
2 | # # Default options for all hosts
3 | # GlobalOptions:
4 | # LogLevel: INFO
5 | # # ----------------------------------------------------------------------
6 | # # Top level standalone host list
7 |
8 | # Hosts:
9 | # - Name: smth_ungrouped # Host example
10 | # HostName: host.example.com # Key: value ssh_config(5)
11 | # User: user2
12 | # IdentityFile: /path/to/key2
13 | # Port: 2223
14 |
15 | ## ----------------------------------------------------------------------
16 | ## Top level group list
17 | Groups:
18 | - Name: work # Some top level group
19 | Options: # ssh_config(5)
20 | User: user
21 | IdentityFile: ~/.ssh/id_rsa_work
22 | Subgroups: # List of subgroups that will inherit "work" Options
23 | - Name: project1
24 | Options:
25 | IdentityFile: ~/.ssh/id_rsa_work_p1
26 | Hosts:
27 | - Name: project1-test
28 | HostName: 192.168.1.2
29 | Port: 2222
30 | - Name: project2
31 | Hosts:
32 | - Name: project2-dev
33 | HostName: 192.168.11.3
34 |
35 | - Name: home # Another top group
36 | Hosts:
37 | - Name: home-gitlab
38 | HostName: gitlab.ratsky.local
--------------------------------------------------------------------------------
/pkg/template.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package sshabu
16 |
17 | import (
18 | "bytes"
19 | _ "embed"
20 |
21 | "io"
22 | "text/template"
23 |
24 | "github.com/Masterminds/sprig/v3"
25 | )
26 |
27 | //go:embed sshabu_example.yaml
28 | var sshabu_example string
29 | //go:embed *.gtpl
30 | var ssh_template string
31 |
32 | func ConfigExample() string{
33 | return sshabu_example
34 | }
35 |
36 | func RenderTemplate(vars interface{}, out io.Writer) error {
37 | t := template.New("openssh_config")
38 |
39 | var funcMap template.FuncMap = map[string]interface{}{}
40 | // copied from: https://github.com/helm/helm/blob/8648ccf5d35d682dcd5f7a9c2082f0aaf071e817/pkg/engine/engine.go#L147-L154
41 | funcMap["include"] = func(name string, data interface{}) (string, error) {
42 | buf := bytes.NewBuffer(nil)
43 | if err := t.ExecuteTemplate(buf, name, data); err != nil {
44 | return "", err
45 | }
46 | return buf.String(), nil
47 | }
48 |
49 | t, err := t.Funcs(sprig.TxtFuncMap()).Funcs(funcMap).Parse(ssh_template)
50 | if err != nil {
51 | return err
52 | }
53 |
54 | err = t.Execute(out, &vars)
55 | if err != nil {
56 | return err
57 | }
58 | return nil
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/template_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package sshabu
16 |
17 | // Import necessary packages for testing
18 | import (
19 | "bytes"
20 | "os"
21 | "path/filepath"
22 | // "strings"
23 | "testing"
24 |
25 | "github.com/spf13/viper"
26 | )
27 | func readExpectedOutput(t *testing.T, filePath string) string {
28 | content, err := os.ReadFile(filePath)
29 | if err != nil {
30 | t.Fatal(err)
31 | }
32 | return string(content)
33 | }
34 | func TestConfigExample(t *testing.T) {
35 | tests := []struct {
36 | name string
37 | want string
38 | }{
39 | {
40 | name: "Example Config Test",
41 | want: readExpectedOutput(t, "sshabu_example.yaml"),
42 | },
43 | // Add more test cases if needed
44 | }
45 | for _, tt := range tests {
46 | t.Run(tt.name, func(t *testing.T) {
47 | if got := ConfigExample(); got != tt.want {
48 | t.Errorf("ConfigExample() = %v, want %v", got, tt.want)
49 | }
50 | })
51 | }
52 | }
53 |
54 | func TestRenderTemplate(t *testing.T) {
55 | // Get the current directory
56 | currentDir, err := os.Getwd()
57 | if err != nil {
58 | t.Fatal(err)
59 | }
60 |
61 | // Create the path to sshabu_example.yaml
62 | yamlFilePath := filepath.Join(currentDir, "sshabu_example.yaml")
63 |
64 | // Set up Viper with the YAML file
65 | viper.SetConfigFile(yamlFilePath)
66 | err = viper.ReadInConfig()
67 | if err != nil {
68 | t.Fatal(err)
69 | }
70 |
71 | // Unmarshal YAML into Shabu
72 | var shabu Shabu
73 | err = viper.UnmarshalExact(&shabu)
74 | if err != nil {
75 | t.Fatal(err)
76 | }
77 |
78 | // Call Boil
79 | err = shabu.Boil()
80 | if err != nil {
81 | t.Fatal(err)
82 | }
83 |
84 | // Create a buffer for RenderTemplate
85 | buf := new(bytes.Buffer)
86 |
87 | // Render the template
88 | err = RenderTemplate(shabu, buf)
89 | if err != nil {
90 | t.Fatal(err)
91 | }
92 |
93 | // Optionally, you can add assertions based on the expected output of RenderTemplate
94 |
95 | // Example assertion:
96 | expectedOutput := `# -----------------------
97 | # RATSKY SSHABU
98 |
99 |
100 | Host project1-test
101 | Hostname 192.168.1.2
102 | IdentityFile ~/.ssh/id_rsa_work_p1
103 | Port 2222
104 | User user
105 |
106 | Host project2-dev
107 | Hostname 192.168.11.3
108 | IdentityFile ~/.ssh/id_rsa_work
109 | User user
110 |
111 | Host home-gitlab
112 | Hostname gitlab.ratsky.local
113 | `
114 | if gotOutput := buf.String(); gotOutput != expectedOutput {
115 | t.Errorf("Rendered output does not match. \nGot: \n%s|\n Want: \n%s|", gotOutput, expectedOutput)
116 | }
117 | }
--------------------------------------------------------------------------------
/pkg/types.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023 Shovra Nikita, Livitsky Andrey
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package sshabu
16 |
17 | import (
18 | "errors"
19 | "reflect"
20 | )
21 |
22 | func inheritOptions(item interface{}, addition interface{}) {
23 | itemValue := reflect.ValueOf(item).Elem()
24 | addValue := reflect.ValueOf(addition).Elem()
25 |
26 | for i := 0; i < itemValue.NumField(); i++ {
27 | if itemValue.Field(i).Interface() == nil {
28 | itemValue.Field(i).Set(addValue.Field(i))
29 | }
30 | }
31 | }
32 |
33 | func findNamesInStruct(value reflect.Value, names *[]string) {
34 | if value.Kind() == reflect.Ptr {
35 | value = value.Elem()
36 | }
37 |
38 | if value.Kind() == reflect.Struct {
39 | structType := value.Type()
40 |
41 | for i := 0; i < value.NumField(); i++ {
42 | fieldValue := value.Field(i)
43 | fieldType := structType.Field(i)
44 |
45 | if fieldType.Name == "Name" {
46 | *names = append(*names, fieldValue.String())
47 | }
48 |
49 | findNamesInStruct(fieldValue, names)
50 | }
51 | } else if value.Kind() == reflect.Slice {
52 | for i := 0; i < value.Len(); i++ {
53 | element := value.Index(i)
54 | findNamesInStruct(element, names)
55 | }
56 | }
57 | }
58 |
59 | type Shabu struct{
60 | Options Options `mapstructure:"globaloptions,omitempty"`
61 | Hosts []Host `mapstructure:"hosts,omitempty"`
62 | Groups []Group `mapstructure:"groups,omitempty"`
63 | // names []string
64 | }
65 |
66 | func (shabu Shabu) FindNamesInShabu() []string {
67 | var names []string
68 |
69 | findNamesInStruct(reflect.ValueOf(shabu), &names)
70 |
71 | return names
72 | }
73 |
74 | func (shabu Shabu) areAllUnique() (bool, *string) {
75 | seen := make(map[string]bool)
76 | items := shabu.FindNamesInShabu()
77 | for _, item := range items {
78 | if seen[item] {
79 | return false, &item // The item is not unique
80 | }
81 | seen[item] = true
82 | }
83 |
84 | return true, nil // All items are unique
85 | }
86 |
87 |
88 | func (shabu *Shabu) Boil() error {
89 | if uniq, name := shabu.areAllUnique(); !uniq{
90 | return errors.New("'Name' fields must be unique - '"+ *name +"' aready used")
91 | }
92 | for i := range shabu.Groups {
93 | shabu.Groups[i].solveGroup(shabu.Groups[i].Options)
94 | }
95 | return nil
96 | }
97 |
98 |
99 | type Host struct{
100 | Options Options `mapstructure:",squash,omitempty"`
101 | Name string `mapstructure:"name"`
102 | }
103 |
104 | func (host *Host) inheritOptions(groupOptions Options) error {
105 | inheritOptions(&host.Options, &groupOptions)
106 | return nil
107 | }
108 |
109 | type Group struct{
110 | Options Options `mapstructure:"options,omitempty"`
111 | Hosts []Host `mapstructure:"hosts,omitempty"`
112 | Name string `mapstructure:"name"`
113 | Subgroups []Group `mapstructure:"subgroups,omitempty"`
114 | }
115 |
116 | func (group *Group) inheritOptions(parentOptions Options) error {
117 | inheritOptions(&group.Options, &parentOptions)
118 | return nil
119 | }
120 |
121 | func (group *Group) solveGroup(parentOptions Options) error {
122 | group.inheritOptions(parentOptions)
123 | for i := range group.Hosts {
124 | group.Hosts[i].inheritOptions(group.Options)
125 | }
126 |
127 | for i := range group.Subgroups {
128 | group.Subgroups[i].solveGroup(group.Options)
129 | }
130 |
131 | return nil
132 | }
133 |
134 | type Option interface{}
135 | type Options struct {
136 | AddKeysToAgent Option `mapstructure:"addkeystoagent,omitempty"`
137 | AddressFamily Option `mapstructure:"addressfamily,omitempty"`
138 | BatchMode Option `mapstructure:"batchmode,omitempty"`
139 | BindAddress Option `mapstructure:"bindaddress,omitempty"`
140 | CanonicalDomains Option `mapstructure:"canonicaldomains,omitempty"`
141 | CanonicalizeFallbackLocal Option `mapstructure:"CanonicalizeFallbackLocal,omitempty"`
142 | CanonicalizeHostname Option `mapstructure:"canonicalizehostname,omitempty"`
143 | CanonicalizeMaxDots Option `mapstructure:"canonicalizemaxdots,omitempty"`
144 | CanonicalizePermittedCNAMEs Option `mapstructure:"CanonicalizePermittedCNAMEs,omitempty"`
145 | CASignatureAlgorithms Option `mapstructure:"casignaturealgorithms,omitempty"`
146 | CertificateFile Option `mapstructure:"certificatefile,omitempty"`
147 | CheckHostIP Option `mapstructure:"checkHostip,omitempty"`
148 | Ciphers Option `mapstructure:"ciphers,omitempty"`
149 | ClearAllForwardings Option `mapstructure:"clearallforwardings,omitempty"`
150 | Compression Option `mapstructure:"compression,omitempty"`
151 | ConnectionAttempts Option `mapstructure:"connectionattempts,omitempty"`
152 | ConnectTimeout Option `mapstructure:"connecttimeout,omitempty"`
153 | ControlMaster Option `mapstructure:"controlmaster,omitempty"`
154 | ControlPath Option `mapstructure:"controlpath,omitempty"`
155 | ControlPersist Option `mapstructure:"controlpersist,omitempty"`
156 | DynamicForward Option `mapstructure:"dynamicforward,omitempty"`
157 | EscapeChar Option `mapstructure:"escapechar,omitempty"`
158 | ExitOnForwardFailure Option `mapstructure:"exitonforwardfailure,omitempty"`
159 | FingerprintHash Option `mapstructure:"fingerprinthash,omitempty"`
160 | ForkAfterAuthentication Option `mapstructure:"forkafterauthentication,omitempty"`
161 | ForwardAgent Option `mapstructure:"forwardagent,omitempty"`
162 | ForwardX11 Option `mapstructure:"forwardx11,omitempty"`
163 | ForwardX11Timeout Option `mapstructure:"forwardx11timeout,omitempty"`
164 | ForwardX11Trusted Option `mapstructure:"forwardx11trusted,omitempty"`
165 | GatewayPorts Option `mapstructure:"gatewayports,omitempty"`
166 | GlobalKnownHostsFile Option `mapstructure:"globalknownhostsfile,omitempty"`
167 | GSSAPIAuthentication Option `mapstructure:"gssapiauthentication,omitempty"`
168 | GSSAPIDelegateCredentials Option `mapstructure:"gssapidelegatecredentials,omitempty"`
169 | HashKnownHosts Option `mapstructure:"hashknownhosts,omitempty"`
170 | Host Option `mapstructure:"host,omitempty"`
171 | HostbasedAcceptedAlgorithms Option `mapstructure:"hostbasedacceptedalgorithms,omitempty"`
172 | HostbasedAuthentication Option `mapstructure:"hostbasedauthentication,omitempty"`
173 | HostKeyAlgorithms Option `mapstructure:"hostkeyalgorithms,omitempty"`
174 | HostKeyAlias Option `mapstructure:"hostkeyalias,omitempty"`
175 | Hostname Option `mapstructure:"hostname,omitempty"`
176 | IdentitiesOnly Option `mapstructure:"IdentitiesOnly,omitempty"`
177 | IdentityAgent Option `mapstructure:"identityagent,omitempty"`
178 | IdentityFile Option `mapstructure:"identityfile,omitempty"`
179 | IPQoS Option `mapstructure:"ipqos,omitempty"`
180 | KbdInteractiveDevices Option `mapstructure:"kbdinteractivedevices,omitempty"`
181 | KexAlgorithms Option `mapstructure:"kexalgorithms,omitempty"`
182 | KnownHostsCommand Option `mapstructure:"knownhostscommand,omitempty"`
183 | LocalCommand Option `mapstructure:"localcommand,omitempty"`
184 | LocalForward Option `mapstructure:"localforward,omitempty"`
185 | LogLevel Option `mapstructure:"loglevel,omitempty"`
186 | MACs Option `mapstructure:"macs,omitempty"`
187 | // Match Option `mapstructure:"match,omitempty"`
188 | NoHostAuthenticationForLocalhost Option `mapstructure:"nohostauthenticationforlocalhost,omitempty"`
189 | NumberOfPasswordPrompts Option `mapstructure:"numberofpasswordprompts,omitempty"`
190 | PasswordAuthentication Option `mapstructure:"passwordauthentication,omitempty"`
191 | PermitLocalCommand Option `mapstructure:"permitlocalcommand,omitempty"`
192 | PermitRemoteOpen Option `mapstructure:"permitremoteopen,omitempty"`
193 | PKCS11Provider Option `mapstructure:"pkcs11provider,omitempty"`
194 | Port Option `mapstructure:"port,omitempty"`
195 | PreferredAuthentications Option `mapstructure:"preferredauthentications,omitempty"`
196 | ProxyCommand Option `mapstructure:"proxycommand,omitempty"`
197 | ProxyJump Option `mapstructure:"proxyjump,omitempty"`
198 | ProxyUseFdpass Option `mapstructure:"proxyusefdpass,omitempty"`
199 | PubkeyAcceptedAlgorithms Option `mapstructure:"pubkeyacceptedalgorithms,omitempty"`
200 | PubkeyAuthentication Option `mapstructure:"pubkeyauthentication,omitempty"`
201 | RekeyLimit Option `mapstructure:"rekeylimit,omitempty"`
202 | RemoteCommand Option `mapstructure:"remotecommand,omitempty"`
203 | RemoteForward Option `mapstructure:"remoteforward,omitempty"`
204 | RequestTTY Option `mapstructure:"requesttty,omitempty"`
205 | SendEnv Option `mapstructure:"sendenv,omitempty"`
206 | ServerAliveInterval Option `mapstructure:"serveraliveinterval,omitempty"`
207 | ServerAliveCountMax Option `mapstructure:"serveralivecountmax,omitempty"`
208 | SessionType Option `mapstructure:"sessionType,omitempty"`
209 | SetEnv Option `mapstructure:"setenv,omitempty"`
210 | StdinNull Option `mapstructure:"stdinnull,omitempty"`
211 | StreamLocalBindMask Option `mapstructure:"streamlocalbindmask,omitempty"`
212 | StreamLocalBindUnlink Option `mapstructure:"streamlocalbindunlink,omitempty"`
213 | StrictHostKeyChecking Option `mapstructure:"stricthostkeychecking,omitempty"`
214 | TCPKeepAlive Option `mapstructure:"tcpkeepalive,omitempty"`
215 | Tunnel Option `mapstructure:"tunnel,omitempty"`
216 | TunnelDevice Option `mapstructure:"tunneldevice,omitempty"`
217 | UpdateHostKeys Option `mapstructure:"updatehostkeys,omitempty"`
218 | UseKeychain Option `mapstructure:"usekeychain,omitempty"`
219 | User Option `mapstructure:"user,omitempty"`
220 | UserKnownHostsFile Option `mapstructure:"userknownhostsfile,omitempty"`
221 | VerifyHostKeyDNS Option `mapstructure:"verifyhostkeydns,omitempty"`
222 | VisualHostKey Option `mapstructure:"visualhostkey,omitempty"`
223 | XAuthLocation Option `mapstructure:"xauthlocation,omitempty"`
224 | }
--------------------------------------------------------------------------------
/pkg/types_test.go:
--------------------------------------------------------------------------------
1 | package sshabu
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func Test_inheritOptions(t *testing.T) {
9 | type args struct {
10 | item interface{}
11 | addition interface{}
12 | }
13 | tests := []struct {
14 | name string
15 | args args
16 | }{
17 | {
18 | name: "No changes when src has nil fields",
19 | args: args{
20 | item: &Options{ // Your source object with some fields set to non-nil values
21 | AddressFamily: "ipv4",
22 | Port: 22,
23 | },
24 | addition: &Options{ // Your destination object with some fields set to nil
25 | AddressFamily: nil,
26 | Port: nil,
27 | },
28 | },
29 | },
30 | {
31 | name: "Copy non-nil fields from src to dst",
32 | args: args{
33 | item: &Options{ // Your source object with all fields set to nil
34 | AddressFamily: nil,
35 | Port: nil,
36 | },
37 | addition: &Options{ // Your destination object with some fields set to non-nil values
38 | AddressFamily: "ipv4",
39 | Port: 22,
40 | },
41 | },
42 | },
43 | }
44 | for _, tt := range tests {
45 | t.Run(tt.name, func(t *testing.T) {
46 | old_item := tt.args.item.(*Options)
47 | inheritOptions(tt.args.item, tt.args.addition)
48 | t.Log(old_item.AddressFamily)
49 | t.Log(tt.args.item.(*Options).AddressFamily)
50 |
51 | // Check if AddressFamily was copied correctly
52 | if old_item.AddressFamily == nil {
53 | // src.AddressFamily is nil, so dst.AddressFamily should remain nil
54 | if tt.args.addition.(*Options).AddressFamily != nil {
55 | t.Errorf("AddressFamily was not copied correctly")
56 | }
57 | } else if tt.args.item.(*Options).AddressFamily != old_item.AddressFamily {
58 | t.Errorf("AddressFamily was not copied correctly")
59 | }
60 |
61 | })
62 | }
63 | }
64 |
65 | func TestShabu_Boil(t *testing.T) {
66 | type fields struct {
67 | Options Options
68 | Hosts []Host
69 | Groups []Group
70 | }
71 | tests := []struct {
72 | name string
73 | fields fields
74 | wantErr bool
75 | }{
76 | {
77 | name: "Test with valid options and groups",
78 | fields: fields{
79 | Options: Options{
80 | AddressFamily: "ipv4",
81 | Port: 22,
82 | },
83 | Hosts: []Host{
84 | {
85 | Name: "Host1",
86 | },
87 | },
88 | Groups: []Group{
89 | {
90 | Name: "Group1",
91 | Options: Options{
92 | AddressFamily: "ipv4",
93 | },
94 | Hosts: []Host{
95 | {
96 | Name: "Host_in_group",
97 | },
98 | },
99 | },
100 | },
101 | },
102 | wantErr: false,
103 | },
104 | {
105 | name: "Invalid Name fields",
106 | fields: fields{
107 | Options: Options{
108 | AddressFamily: "ipv4",
109 | Port: 22,
110 | },
111 | Hosts: []Host{
112 | {
113 | Name: "Host1",
114 | },
115 | },
116 | Groups: []Group{
117 | {
118 | Name: "Host1",
119 | Options: Options{
120 | AddressFamily: "ipv4",
121 | },
122 | Hosts: []Host{
123 | {
124 | Name: "Host_in_group",
125 | },
126 | },
127 | },
128 | },
129 | },
130 | wantErr: true,
131 | },
132 | // Other cases
133 | }
134 | for _, tt := range tests {
135 | t.Run(tt.name, func(t *testing.T) {
136 | shabu := &Shabu{
137 | Options: tt.fields.Options,
138 | Hosts: tt.fields.Hosts,
139 | Groups: tt.fields.Groups,
140 | }
141 | err := shabu.Boil()
142 | t.Log(shabu.Groups[0].Hosts[0].Options.AddressFamily)
143 | if (err != nil) != tt.wantErr {
144 | t.Errorf("Shabu.Boil() error = %v, wantErr %v", err, tt.wantErr)
145 | }
146 | })
147 | }
148 | }
149 |
150 | func TestHost_inheritOptions(t *testing.T) {
151 | type fields struct {
152 | Options Options
153 | Name string
154 | }
155 | type args struct {
156 | groupOptions Options
157 | }
158 | tests := []struct {
159 | name string
160 | fields fields
161 | args args
162 | wantErr bool
163 | }{
164 | {
165 | name: "Test with valid options and groups",
166 | fields: fields{
167 | Options: Options{
168 | AddressFamily: "ipv4",
169 | },
170 | Name: "Host123",
171 | },
172 | args: args{
173 | groupOptions: Options{
174 | User: "lvtsky",
175 | },
176 | },
177 | wantErr: false,
178 | },
179 | // Add more test cases if needed
180 | }
181 | for _, tt := range tests {
182 | t.Run(tt.name, func(t *testing.T) {
183 | host := &Host{
184 | Options: tt.fields.Options,
185 | Name: tt.fields.Name,
186 | }
187 | if err := host.inheritOptions(tt.args.groupOptions); (err != nil) != tt.wantErr {
188 | t.Errorf("Host.inheritOptions() error = %v, wantErr %v", err, tt.wantErr)
189 | }
190 |
191 | // Add assertions to verify that options were inherited correctly
192 | if host.Options.User != "lvtsky" {
193 | t.Errorf("User field was not inherited correctly")
194 | }
195 | })
196 | }
197 | }
198 |
199 | func TestGroup_inheritOptions(t *testing.T) {
200 | type fields struct {
201 | Options Options
202 | Hosts []Host
203 | Name string
204 | Subgroups []Group
205 | }
206 | type args struct {
207 | parentOptions Options
208 | }
209 | tests := []struct {
210 | name string
211 | fields fields
212 | args args
213 | wantErr bool
214 | }{
215 | {
216 | name: "Test with valid parent options",
217 | fields: fields{
218 | Options: Options{
219 | User: "lvtsky",
220 | },
221 | Name: "Group123",
222 | Subgroups: []Group{
223 | {
224 | Name: "Subgroup1",
225 | Options: Options{
226 | Port: 22,
227 | },
228 | },
229 | },
230 | },
231 | args: args{
232 | parentOptions: Options{
233 | AddressFamily: "ipv4",
234 | },
235 | },
236 | wantErr: false,
237 | },
238 | // Add more test cases if needed
239 | }
240 | for _, tt := range tests {
241 | t.Run(tt.name, func(t *testing.T) {
242 | group := &Group{
243 | Options: tt.fields.Options,
244 | Hosts: tt.fields.Hosts,
245 | Name: tt.fields.Name,
246 | Subgroups: tt.fields.Subgroups,
247 | }
248 | if err := group.inheritOptions(tt.args.parentOptions); (err != nil) != tt.wantErr {
249 | t.Errorf("Group.inheritOptions() error = %v, wantErr %v", err, tt.wantErr)
250 | }
251 |
252 | // Add assertions to verify that options were inherited correctly
253 | if group.Options.User != "lvtsky" {
254 | t.Errorf("User field was not inherited correctly")
255 | }
256 |
257 | // Add similar checks for other fields or nested structures
258 | })
259 | }
260 | }
261 |
262 | func TestGroup_solveGroup(t *testing.T) {
263 | type fields struct {
264 | Options Options
265 | Hosts []Host
266 | Name string
267 | Subgroups []Group
268 | }
269 | type args struct {
270 | parentOptions Options
271 | }
272 | tests := []struct {
273 | name string
274 | fields fields
275 | args args
276 | wantErr bool
277 | }{
278 | {
279 | name: "Test with valid options and subgroups",
280 | fields: fields{
281 | Options: Options{
282 | User: "lvtsky",
283 | },
284 | Name: "Group123",
285 | Subgroups: []Group{
286 | {
287 | Name: "Subgroup1",
288 | Options: Options{
289 | Port: 22,
290 | },
291 | },
292 | },
293 | },
294 | args: args{
295 | parentOptions: Options{
296 | AddressFamily: "ipv4",
297 | },
298 | },
299 | wantErr: false,
300 | },
301 | // Add more test cases if needed
302 | }
303 | for _, tt := range tests {
304 | t.Run(tt.name, func(t *testing.T) {
305 | group := &Group{
306 | Options: tt.fields.Options,
307 | Hosts: tt.fields.Hosts,
308 | Name: tt.fields.Name,
309 | Subgroups: tt.fields.Subgroups,
310 | }
311 | if err := group.solveGroup(tt.args.parentOptions); (err != nil) != tt.wantErr {
312 | t.Errorf("Group.solveGroup() error = %v, wantErr %v", err, tt.wantErr)
313 | }
314 |
315 | // Add assertions to verify that options were inherited correctly
316 | if group.Options.User != "lvtsky" {
317 | t.Errorf("User field was not inherited correctly")
318 | }
319 |
320 | // Add similar checks for other fields or nested structures
321 | })
322 | }
323 | }
324 |
325 | func TestShabu_FindNamesInShabu(t *testing.T) {
326 | type fields struct {
327 | Options Options
328 | Hosts []Host
329 | Groups []Group
330 | }
331 | tests := []struct {
332 | name string
333 | fields fields
334 | want []string
335 | }{
336 | {
337 | name: "Test with valid names in Shabu",
338 | fields: fields{
339 | Options: Options{
340 | User: "lvtsky",
341 | },
342 | Hosts: []Host{
343 | {
344 | Name: "Host1",
345 | },
346 | },
347 | Groups: []Group{
348 | {
349 | Name: "Group1",
350 | },
351 | {
352 | Name: "Group2",
353 | },
354 | },
355 | },
356 | want: []string{"Host1", "Group1", "Group2"},
357 | },
358 | // Add more test cases if needed
359 | }
360 | for _, tt := range tests {
361 | t.Run(tt.name, func(t *testing.T) {
362 | shabu := Shabu{
363 | Options: tt.fields.Options,
364 | Hosts: tt.fields.Hosts,
365 | Groups: tt.fields.Groups,
366 | }
367 | if got := shabu.FindNamesInShabu(); !reflect.DeepEqual(got, tt.want) {
368 | t.Errorf("Shabu.FindNamesInShabu() = %v, want %v", got, tt.want)
369 | }
370 | })
371 | }
372 | }
373 |
374 |
--------------------------------------------------------------------------------
/scripts/completion.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # scripts/completions.sh
3 | set -e
4 | rm -rf completions
5 | mkdir completions
6 | # TODO: replace your-cli with your binary name
7 | for sh in bash zsh fish; do
8 | go run main.go completion "$sh" >"completions/sshabu.$sh"
9 | done
--------------------------------------------------------------------------------