├── docs
├── sshabu-quick.gif
└── license.tpl
├── .gitignore
├── scripts
└── completion.sh
├── main.go
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ ├── release.yml
│ └── tests.yml
├── .gitlab-ci.yml
├── go.mod
├── pkg
├── sshabu_example.yaml
├── template.go
├── checker.go
├── compare
│ ├── compare_test.go
│ └── compare.go
├── template_test.go
├── checker_test.go
├── openssh_conf.gtpl
├── types_test.go
└── types.go
├── cmd
├── init.go
├── edit.go
├── connect.go
├── root.go
└── apply.go
├── .goreleaser.yaml
├── README.md
├── LICENSE
└── go.sum
/docs/sshabu-quick.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ratsky-oss/sshabu/HEAD/docs/sshabu-quick.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | openssh.*
2 | sshabu.yaml
3 | *.drowio
4 | *.bkp
5 | dist/
6 | .DS_Store
7 | completions
8 | .vscode/
9 | sshabu
10 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |