feat: add cross-platform release packaging
This commit is contained in:
parent
48c0057d11
commit
3e82183d5c
15
Makefile
15
Makefile
|
|
@ -1,6 +1,7 @@
|
||||||
APP=sshkeeper
|
APP=sshkeeper
|
||||||
|
RELEASE_CHECK_DIR ?= /tmp/sshkeeper-release-check
|
||||||
|
|
||||||
.PHONY: build run test vet fmt clean install
|
.PHONY: build run test vet fmt clean install release-check
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -o bin/$(APP) .
|
go build -o bin/$(APP) .
|
||||||
|
|
@ -22,3 +23,15 @@ clean:
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go build -o $(HOME)/.local/bin/$(APP) .
|
go build -o $(HOME)/.local/bin/$(APP) .
|
||||||
|
|
||||||
|
release-check:
|
||||||
|
rm -rf $(RELEASE_CHECK_DIR)
|
||||||
|
mkdir -p $(RELEASE_CHECK_DIR)
|
||||||
|
go test ./...
|
||||||
|
go vet ./...
|
||||||
|
CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP) .
|
||||||
|
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP)-linux-amd64 .
|
||||||
|
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP)-linux-arm64 .
|
||||||
|
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP)-darwin-amd64 .
|
||||||
|
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP)-darwin-arm64 .
|
||||||
|
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP)-windows-amd64.exe .
|
||||||
|
|
|
||||||
32
README.md
32
README.md
|
|
@ -1,7 +1,8 @@
|
||||||
# sshkeeper
|
# sshkeeper
|
||||||
|
|
||||||
`sshkeeper` is a Linux console manager for SSH profiles, secrets, and quick
|
`sshkeeper` is a console manager for SSH profiles, secrets, and quick OpenSSH
|
||||||
OpenSSH launches. It does not replace OpenSSH; it keeps connection metadata in a
|
launches. Primary target: Linux. macOS builds are available, and Windows builds
|
||||||
|
are experimental. It does not replace OpenSSH; it keeps connection metadata in a
|
||||||
local SQLite database, keeps passwords/passphrases in an encrypted vault, and
|
local SQLite database, keeps passwords/passphrases in an encrypted vault, and
|
||||||
starts the system `ssh` client with the right options.
|
starts the system `ssh` client with the right options.
|
||||||
|
|
||||||
|
|
@ -42,10 +43,24 @@ Or use the build scripts:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh # Build binary to bin/
|
./build.sh # Build binary to bin/
|
||||||
./release.sh # Build release tarballs to dist/
|
./release.sh # Build release archives to dist/
|
||||||
```
|
```
|
||||||
|
|
||||||
Requirements: Go 1.25+, Linux x86_64, system OpenSSH.
|
Requirements: Go 1.25+ and system OpenSSH.
|
||||||
|
|
||||||
|
Platform status:
|
||||||
|
|
||||||
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Linux | Primary target | `linux/amd64` and `linux/arm64` release tarballs are available. |
|
||||||
|
| macOS | Supported, needs verification on target machines | `darwin/amd64` and `darwin/arm64` release tarballs are available. Requires system `ssh` client. Homebrew formula planned. |
|
||||||
|
| Windows | Experimental | Requires OpenSSH Client available as `ssh.exe` in `PATH`. Password/key-passphrase PTY flows are not validated on Windows. |
|
||||||
|
|
||||||
|
On Windows, install OpenSSH Client via Windows Optional Features or PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
**Source repositories:**
|
**Source repositories:**
|
||||||
- Main public: [github.com/mirivlad/sshkeeper](https://github.com/mirivlad/sshkeeper)
|
- Main public: [github.com/mirivlad/sshkeeper](https://github.com/mirivlad/sshkeeper)
|
||||||
|
|
@ -55,8 +70,7 @@ Requirements: Go 1.25+, Linux x86_64, system OpenSSH.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar -xzf sshkeeper_v0.2.0_linux_amd64.tar.gz
|
tar -xzf sshkeeper_v0.2.0_linux_amd64.tar.gz
|
||||||
chmod +x sshkeeper-linux-amd64
|
sudo install -m 0755 sshkeeper_v0.2.0_linux_amd64/sshkeeper /usr/local/bin/sshkeeper
|
||||||
sudo install -m 0755 sshkeeper-linux-amd64 /usr/local/bin/sshkeeper
|
|
||||||
sshkeeper
|
sshkeeper
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -285,10 +299,13 @@ If `XDG_CONFIG_HOME` or `XDG_DATA_HOME` are set, sshkeeper stores data under
|
||||||
```bash
|
```bash
|
||||||
go test ./...
|
go test ./...
|
||||||
go build -o bin/sshkeeper .
|
go build -o bin/sshkeeper .
|
||||||
|
make release-check
|
||||||
```
|
```
|
||||||
|
|
||||||
`bin/` is ignored by git.
|
`bin/` is ignored by git.
|
||||||
|
|
||||||
|
For release packaging details, see [docs/release.md](docs/release.md).
|
||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
@ -302,8 +319,9 @@ sshkeeper/
|
||||||
├── internal/vault/ # Encrypted vault
|
├── internal/vault/ # Encrypted vault
|
||||||
├── internal/tunnel/ # Tunnel state management
|
├── internal/tunnel/ # Tunnel state management
|
||||||
├── docs/guide.md # User guide
|
├── docs/guide.md # User guide
|
||||||
|
├── docs/release.md # Release packaging guide
|
||||||
├── build.sh # Build binary to bin/
|
├── build.sh # Build binary to bin/
|
||||||
├── release.sh # Build release tarballs to dist/
|
├── release.sh # Build release archives to dist/
|
||||||
└── main.go
|
└── main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ var (
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "sshkeeper",
|
Use: "sshkeeper",
|
||||||
Short: "sshkeeper — SSH connection manager",
|
Short: "sshkeeper — SSH connection manager",
|
||||||
Long: `sshkeeper is a console SSH connection manager for Linux.
|
Long: `sshkeeper is a console SSH connection manager.
|
||||||
|
Linux is the primary target; macOS builds are available and Windows is experimental.
|
||||||
It manages server profiles, secrets, and provides a convenient way
|
It manages server profiles, secrets, and provides a convenient way
|
||||||
to launch SSH sessions using the system OpenSSH client.`,
|
to launch SSH sessions using the system OpenSSH client.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
## Что такое sshkeeper
|
## Что такое sshkeeper
|
||||||
|
|
||||||
sshkeeper — это консольный менеджер SSH-подключений для Linux. Он хранит профили серверов, секреты (пароли, фразы от ключей) и запускает системный `ssh` с нужными опциями.
|
sshkeeper — это консольный менеджер SSH-подключений. Основная целевая платформа — Linux; сборки для macOS доступны, Windows-сборка пока экспериментальная. Он хранит профили серверов, секреты (пароли, фразы от ключей) и запускает системный `ssh` с нужными опциями.
|
||||||
|
|
||||||
**Чем sshkeeper НЕ является:**
|
**Чем sshkeeper НЕ является:**
|
||||||
- Это не Ansible — он не настраивает серверы и не пушит файлы
|
- Это не Ansible — он не настраивает серверы и не пушит файлы
|
||||||
|
|
@ -63,14 +63,27 @@ go build -o ~/.local/bin/sshkeeper .
|
||||||
./release.sh # сборка релизных архивов в dist/
|
./release.sh # сборка релизных архивов в dist/
|
||||||
```
|
```
|
||||||
|
|
||||||
**Требования:** Go 1.25+, Linux x86_64, системный OpenSSH.
|
**Требования:** Go 1.25+ и системный OpenSSH.
|
||||||
|
|
||||||
|
Статус платформ:
|
||||||
|
|
||||||
|
| Платформа | Статус | Примечание |
|
||||||
|
|-----------|--------|------------|
|
||||||
|
| Linux | Основная целевая платформа | Архивы `linux/amd64` и `linux/arm64`. |
|
||||||
|
| macOS | Сборки доступны, требуется проверка на целевых машинах | Архивы `darwin/amd64` и `darwin/arm64`, нужен системный `ssh`. Homebrew formula запланирована. |
|
||||||
|
| Windows | Experimental | Нужен OpenSSH Client как `ssh.exe` в `PATH`; password/key-passphrase PTY-сценарии на Windows пока не подтверждены. |
|
||||||
|
|
||||||
|
На Windows OpenSSH Client можно установить через Windows Optional Features или PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
### Из релиза (после публикации v0.2.0)
|
### Из релиза (после публикации v0.2.0)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar -xzf sshkeeper_v0.2.0_linux_amd64.tar.gz
|
tar -xzf sshkeeper_v0.2.0_linux_amd64.tar.gz
|
||||||
chmod +x sshkeeper-linux-amd64
|
sudo install -m 0755 sshkeeper_v0.2.0_linux_amd64/sshkeeper /usr/local/bin/sshkeeper
|
||||||
sudo install -m 0755 sshkeeper-linux-amd64 /usr/local/bin/sshkeeper
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -192,7 +205,8 @@ sshkeeper — Quick Help
|
||||||
sshkeeper — Full Help
|
sshkeeper — Full Help
|
||||||
|
|
||||||
What is sshkeeper
|
What is sshkeeper
|
||||||
sshkeeper is a Linux console SSH connection manager.
|
sshkeeper is a console SSH connection manager.
|
||||||
|
Linux is the primary target; macOS builds are available and Windows is experimental.
|
||||||
It stores server profiles, secrets, and launches the system ssh client.
|
It stores server profiles, secrets, and launches the system ssh client.
|
||||||
|
|
||||||
Navigation
|
Navigation
|
||||||
|
|
@ -782,6 +796,9 @@ go test ./...
|
||||||
# Сборка
|
# Сборка
|
||||||
go build -o bin/sshkeeper .
|
go build -o bin/sshkeeper .
|
||||||
|
|
||||||
|
# Проверка релизной кросс-сборки
|
||||||
|
make release-check
|
||||||
|
|
||||||
# Или через скрипты
|
# Или через скрипты
|
||||||
./build.sh # сборка в bin/
|
./build.sh # сборка в bin/
|
||||||
./release.sh # релизные архивы в dist/
|
./release.sh # релизные архивы в dist/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
# Release Packaging
|
||||||
|
|
||||||
|
This document describes the manual release flow for sshkeeper.
|
||||||
|
|
||||||
|
## Create a Tag
|
||||||
|
|
||||||
|
Use a semantic version tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short
|
||||||
|
git tag -a v0.2.0 -m "sshkeeper v0.2.0"
|
||||||
|
git push origin v0.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The release script uses `git describe --tags --always --dirty` by default. You
|
||||||
|
can also pass the version explicitly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./release.sh v0.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VERSION=v0.2.0 ./release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
For reproducible archives, the script uses `SOURCE_DATE_EPOCH`. By default it
|
||||||
|
uses the timestamp of the latest git commit. To force a specific timestamp:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SOURCE_DATE_EPOCH=1760000000 ./release.sh v0.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run Release Checks
|
||||||
|
|
||||||
|
Before packaging, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make release-check
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs:
|
||||||
|
|
||||||
|
- `go test ./...`
|
||||||
|
- `go vet ./...`
|
||||||
|
- native `go build`
|
||||||
|
- `linux/amd64` cross-build with `CGO_ENABLED=0`
|
||||||
|
- `linux/arm64` cross-build with `CGO_ENABLED=0`
|
||||||
|
- `darwin/amd64` cross-build with `CGO_ENABLED=0`
|
||||||
|
- `darwin/arm64` cross-build with `CGO_ENABLED=0`
|
||||||
|
- `windows/amd64` cross-build with `CGO_ENABLED=0`
|
||||||
|
|
||||||
|
## Build Artifacts
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./release.sh v0.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected files in `dist/`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sshkeeper_v0.2.0_linux_amd64.tar.gz
|
||||||
|
sshkeeper_v0.2.0_linux_arm64.tar.gz
|
||||||
|
sshkeeper_v0.2.0_darwin_amd64.tar.gz
|
||||||
|
sshkeeper_v0.2.0_darwin_arm64.tar.gz
|
||||||
|
sshkeeper_v0.2.0_windows_amd64.zip
|
||||||
|
checksums.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Each archive contains:
|
||||||
|
|
||||||
|
- `sshkeeper` or `sshkeeper.exe`
|
||||||
|
- `README.md`
|
||||||
|
- `LICENSE`
|
||||||
|
- `docs/guide.md`
|
||||||
|
|
||||||
|
## Verify Checksums
|
||||||
|
|
||||||
|
From the `dist/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sha256sum -c checksums.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result: every archive reports `OK`.
|
||||||
|
|
||||||
|
## Publish in GitHub Release
|
||||||
|
|
||||||
|
Upload these files to the release:
|
||||||
|
|
||||||
|
- all five platform archives
|
||||||
|
- `checksums.txt`
|
||||||
|
|
||||||
|
Release notes should mention platform status:
|
||||||
|
|
||||||
|
- Linux is the primary target.
|
||||||
|
- macOS builds are available as tar.gz for amd64 and arm64 and require the
|
||||||
|
system `ssh` client.
|
||||||
|
- Windows build is experimental and requires OpenSSH Client available as
|
||||||
|
`ssh.exe` in `PATH`.
|
||||||
|
|
||||||
|
## Packaging TODO
|
||||||
|
|
||||||
|
Prepare these package channels after the first archive-based release:
|
||||||
|
|
||||||
|
- deb package
|
||||||
|
- Arch PKGBUILD / AUR
|
||||||
|
- rpm later
|
||||||
|
- Homebrew tap
|
||||||
|
- Scoop manifest
|
||||||
|
- Winget later
|
||||||
|
|
@ -3,6 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
)
|
)
|
||||||
|
|
@ -34,7 +35,7 @@ type UIConfig struct {
|
||||||
func defaultConfig() *Config {
|
func defaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
SSH: SSHConfig{
|
SSH: SSHConfig{
|
||||||
Binary: "/usr/bin/ssh",
|
Binary: defaultSSHBinary(),
|
||||||
ConnectTimeoutSec: 10,
|
ConnectTimeoutSec: 10,
|
||||||
TestCommand: "echo SSHKEEPER_OK",
|
TestCommand: "echo SSHKEEPER_OK",
|
||||||
},
|
},
|
||||||
|
|
@ -47,6 +48,13 @@ func defaultConfig() *Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultSSHBinary() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return "ssh.exe"
|
||||||
|
}
|
||||||
|
return "/usr/bin/ssh"
|
||||||
|
}
|
||||||
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
cfg := defaultConfig()
|
cfg := defaultConfig()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mirivlad/sshkeeper/internal/config"
|
"github.com/mirivlad/sshkeeper/internal/config"
|
||||||
|
|
@ -12,7 +13,34 @@ import (
|
||||||
|
|
||||||
type VaultFunc func(serverAlias string, secretType string) (string, error)
|
type VaultFunc func(serverAlias string, secretType string) (string, error)
|
||||||
|
|
||||||
|
const windowsOpenSSHInstallHint = "Install OpenSSH Client via Windows Optional Features or PowerShell:\nAdd-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0"
|
||||||
|
|
||||||
|
func EnsureSSHBinary(binary string) error {
|
||||||
|
return validateSSHBinaryForOS(runtime.GOOS, binary, exec.LookPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSSHBinaryForOS(goos string, binary string, lookPath func(string) (string, error)) error {
|
||||||
|
if goos == "windows" {
|
||||||
|
if _, err := lookPath("ssh.exe"); err != nil {
|
||||||
|
return fmt.Errorf("ssh.exe not found in PATH. Windows build is experimental and requires OpenSSH Client.\n%s", windowsOpenSSHInstallHint)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(binary) == "" {
|
||||||
|
binary = "ssh"
|
||||||
|
}
|
||||||
|
if _, err := lookPath(binary); err != nil {
|
||||||
|
return fmt.Errorf("ssh binary not found (%s): %w", binary, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func Connect(cfg *config.Config, server *model.Server, getVault VaultFunc) error {
|
func Connect(cfg *config.Config, server *model.Server, getVault VaultFunc) error {
|
||||||
|
if err := EnsureSSHBinary(cfg.SSH.Binary); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
args := BuildSSHArgsSimple(server)
|
args := BuildSSHArgsSimple(server)
|
||||||
if strings.TrimSpace(server.StartupCommand) != "" {
|
if strings.TrimSpace(server.StartupCommand) != "" {
|
||||||
args = append(args, server.StartupCommand)
|
args = append(args, server.StartupCommand)
|
||||||
|
|
@ -49,6 +77,10 @@ func Connect(cfg *config.Config, server *model.Server, getVault VaultFunc) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunCommand(cfg *config.Config, server *model.Server, getVault VaultFunc, command string) error {
|
func RunCommand(cfg *config.Config, server *model.Server, getVault VaultFunc, command string) error {
|
||||||
|
if err := EnsureSSHBinary(cfg.SSH.Binary); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
args := BuildSSHArgsSimple(server)
|
args := BuildSSHArgsSimple(server)
|
||||||
args = append(args, command)
|
args = append(args, command)
|
||||||
|
|
||||||
|
|
@ -78,6 +110,10 @@ func RunCommand(cfg *config.Config, server *model.Server, getVault VaultFunc, co
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunCommandOutput(cfg *config.Config, server *model.Server, getVault VaultFunc, command string) (string, error) {
|
func RunCommandOutput(cfg *config.Config, server *model.Server, getVault VaultFunc, command string) (string, error) {
|
||||||
|
if err := EnsureSSHBinary(cfg.SSH.Binary); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
args := BuildSSHArgsSimple(server)
|
args := BuildSSHArgsSimple(server)
|
||||||
args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", cfg.SSH.ConnectTimeoutSec))
|
args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", cfg.SSH.ConnectTimeoutSec))
|
||||||
|
|
||||||
|
|
@ -116,6 +152,10 @@ func RunCommandOutput(cfg *config.Config, server *model.Server, getVault VaultFu
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test(cfg *config.Config, server *model.Server, getVault VaultFunc) (bool, string) {
|
func Test(cfg *config.Config, server *model.Server, getVault VaultFunc) (bool, string) {
|
||||||
|
if err := EnsureSSHBinary(cfg.SSH.Binary); err != nil {
|
||||||
|
return false, err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
args := BuildSSHArgsSimple(server)
|
args := BuildSSHArgsSimple(server)
|
||||||
args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", cfg.SSH.ConnectTimeoutSec))
|
args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", cfg.SSH.ConnectTimeoutSec))
|
||||||
|
|
||||||
|
|
@ -179,6 +219,10 @@ func testWithPassword(cfg *config.Config, args []string, password string) (bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectWithArgs(cfg *config.Config, args []string, vaultFunc VaultFunc, server *model.Server) error {
|
func ConnectWithArgs(cfg *config.Config, args []string, vaultFunc VaultFunc, server *model.Server) error {
|
||||||
|
if err := EnsureSSHBinary(cfg.SSH.Binary); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
switch server.AuthMethod {
|
switch server.AuthMethod {
|
||||||
case model.AuthPassword:
|
case model.AuthPassword:
|
||||||
password, err := vaultFunc(server.Alias, "ssh_password")
|
password, err := vaultFunc(server.Alias, "ssh_password")
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,40 @@ func TestConnectRunsStartupCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateSSHBinaryForWindowsRequiresOpenSSHClient(t *testing.T) {
|
||||||
|
err := validateSSHBinaryForOS("windows", "ssh.exe", func(name string) (string, error) {
|
||||||
|
if name != "ssh.exe" {
|
||||||
|
t.Fatalf("expected lookup for ssh.exe, got %q", name)
|
||||||
|
}
|
||||||
|
return "", os.ErrNotExist
|
||||||
|
})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected missing ssh.exe error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "OpenSSH Client") {
|
||||||
|
t.Fatalf("expected OpenSSH Client guidance, got %q", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0") {
|
||||||
|
t.Fatalf("expected PowerShell install command, got %q", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSSHBinaryForNonWindowsChecksConfiguredBinary(t *testing.T) {
|
||||||
|
var lookedUp string
|
||||||
|
err := validateSSHBinaryForOS("linux", "/usr/bin/ssh", func(name string) (string, error) {
|
||||||
|
lookedUp = name
|
||||||
|
return "/usr/bin/ssh", nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("validate ssh binary: %v", err)
|
||||||
|
}
|
||||||
|
if lookedUp != "/usr/bin/ssh" {
|
||||||
|
t.Fatalf("expected lookup for configured binary, got %q", lookedUp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildSSHArgs_Simple(t *testing.T) {
|
func TestBuildSSHArgs_Simple(t *testing.T) {
|
||||||
server := &model.Server{Host: "example.org", Port: 22, User: "root"}
|
server := &model.Server{Host: "example.org", Port: 22, User: "root"}
|
||||||
args := BuildSSHArgsSimple(server)
|
args := BuildSSHArgsSimple(server)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
package ssh
|
package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
package ssh
|
package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const windowsPTYUnsupportedMessage = "password and key-passphrase auth are not supported on Windows experimental builds; use key or agent auth with OpenSSH Client"
|
||||||
|
|
||||||
|
func ConnectWithPassword(sshBinary string, args []string, password string) error {
|
||||||
|
return fmt.Errorf("%s", windowsPTYUnsupportedMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectWithPasswordAndRead(sshBinary string, args []string, password string, timeoutSec int) (bool, string) {
|
||||||
|
return false, windowsPTYUnsupportedMessage
|
||||||
|
}
|
||||||
|
|
@ -159,7 +159,8 @@ func (m *fullHelpModel) View() string {
|
||||||
rows [][2]string
|
rows [][2]string
|
||||||
}{
|
}{
|
||||||
{"What is sshkeeper", [][2]string{
|
{"What is sshkeeper", [][2]string{
|
||||||
{"", "sshkeeper is a Linux console SSH connection manager."},
|
{"", "sshkeeper is a console SSH connection manager."},
|
||||||
|
{"", "Linux is the primary target; macOS builds are available and Windows is experimental."},
|
||||||
{"", "It stores server profiles, secrets, and launches the system ssh client."},
|
{"", "It stores server profiles, secrets, and launches the system ssh client."},
|
||||||
{"", ""},
|
{"", ""},
|
||||||
}},
|
}},
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,10 @@ func Start(cfg *config.Config, server *model.Server, forwards []*model.Forward,
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if err := ssh.EnsureSSHBinary(cfg.SSH.Binary); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Filter enabled forwards
|
// Filter enabled forwards
|
||||||
var active []*model.Forward
|
var active []*model.Forward
|
||||||
for _, f := range forwards {
|
for _, f := range forwards {
|
||||||
|
|
|
||||||
71
release.sh
71
release.sh
|
|
@ -4,26 +4,73 @@ set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
APP=sshkeeper
|
APP=sshkeeper
|
||||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
VERSION=${VERSION:-${1:-$(git describe --tags --always --dirty 2>/dev/null || echo "dev")}}
|
||||||
LDFLAGS="-s -w -X main.version=${VERSION}"
|
LDFLAGS="-s -w -X main.version=${VERSION}"
|
||||||
DIST_DIR="dist"
|
DIST_DIR="dist"
|
||||||
|
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-$(git log -1 --format=%ct 2>/dev/null || date +%s)}
|
||||||
|
|
||||||
echo "==> Building release ${APP} ${VERSION}..."
|
echo "==> Building release ${APP} ${VERSION}..."
|
||||||
|
echo "==> SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}"
|
||||||
|
|
||||||
rm -rf "${DIST_DIR}"
|
rm -rf "${DIST_DIR}"
|
||||||
mkdir -p "${DIST_DIR}"
|
mkdir -p "${DIST_DIR}"
|
||||||
|
|
||||||
# Linux amd64
|
package_docs() {
|
||||||
echo "==> linux/amd64..."
|
local package_dir="$1"
|
||||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "${LDFLAGS}" -o "${DIST_DIR}/${APP}" .
|
|
||||||
tar -czf "${DIST_DIR}/${APP}_${VERSION}_linux_amd64.tar.gz" -C "${DIST_DIR}" "${APP}"
|
|
||||||
rm -f "${DIST_DIR}/${APP}"
|
|
||||||
|
|
||||||
# Linux arm64
|
cp README.md LICENSE "${package_dir}/"
|
||||||
echo "==> linux/arm64..."
|
if [[ -f docs/guide.md ]]; then
|
||||||
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags "${LDFLAGS}" -o "${DIST_DIR}/${APP}" .
|
mkdir -p "${package_dir}/docs"
|
||||||
tar -czf "${DIST_DIR}/${APP}_${VERSION}_linux_arm64.tar.gz" -C "${DIST_DIR}" "${APP}"
|
cp docs/guide.md "${package_dir}/docs/"
|
||||||
rm -f "${DIST_DIR}/${APP}"
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_package() {
|
||||||
|
local package_dir="$1"
|
||||||
|
find "${package_dir}" -exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} +
|
||||||
|
}
|
||||||
|
|
||||||
|
build_tarball() {
|
||||||
|
local goos="$1"
|
||||||
|
local goarch="$2"
|
||||||
|
local package_dir="${DIST_DIR}/${APP}_${VERSION}_${goos}_${goarch}"
|
||||||
|
local archive="${DIST_DIR}/${APP}_${VERSION}_${goos}_${goarch}.tar.gz"
|
||||||
|
|
||||||
|
echo "==> ${goos}/${goarch}..."
|
||||||
|
rm -rf "${package_dir}"
|
||||||
|
mkdir -p "${package_dir}"
|
||||||
|
|
||||||
|
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED=0 go build -trimpath -ldflags "${LDFLAGS}" -o "${package_dir}/${APP}" .
|
||||||
|
package_docs "${package_dir}"
|
||||||
|
normalize_package "${package_dir}"
|
||||||
|
tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@${SOURCE_DATE_EPOCH}" -cf - -C "${DIST_DIR}" "$(basename "${package_dir}")" | gzip -n > "${archive}"
|
||||||
|
rm -rf "${package_dir}"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_zip() {
|
||||||
|
local goos="$1"
|
||||||
|
local goarch="$2"
|
||||||
|
local package_dir="${DIST_DIR}/${APP}_${VERSION}_${goos}_${goarch}"
|
||||||
|
local archive="${DIST_DIR}/${APP}_${VERSION}_${goos}_${goarch}.zip"
|
||||||
|
|
||||||
|
echo "==> ${goos}/${goarch}..."
|
||||||
|
rm -rf "${package_dir}"
|
||||||
|
mkdir -p "${package_dir}"
|
||||||
|
|
||||||
|
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED=0 go build -trimpath -ldflags "${LDFLAGS}" -o "${package_dir}/${APP}.exe" .
|
||||||
|
package_docs "${package_dir}"
|
||||||
|
normalize_package "${package_dir}"
|
||||||
|
(cd "${DIST_DIR}" && find "$(basename "${package_dir}")" -print | sort | zip -X -q "$(basename "${archive}")" -@)
|
||||||
|
rm -rf "${package_dir}"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_tarball linux amd64
|
||||||
|
build_tarball linux arm64
|
||||||
|
build_tarball darwin amd64
|
||||||
|
build_tarball darwin arm64
|
||||||
|
build_zip windows amd64
|
||||||
|
|
||||||
|
(cd "${DIST_DIR}" && sha256sum *.tar.gz *.zip > checksums.txt)
|
||||||
|
|
||||||
echo "==> Done."
|
echo "==> Done."
|
||||||
ls -lh "${DIST_DIR}/"*.tar.gz
|
ls -lh "${DIST_DIR}/"*.tar.gz "${DIST_DIR}/"*.zip "${DIST_DIR}/checksums.txt"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue