fix: make package install authoritative
This commit is contained in:
parent
b481bb9f3e
commit
dd7ea6012d
|
|
@ -46,6 +46,10 @@ jobs:
|
||||||
- name: go test
|
- name: go test
|
||||||
run: go test ./... -count=1
|
run: go test ./... -count=1
|
||||||
|
|
||||||
|
- name: package migration test
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: ./packaging/scripts/test-legacy-migration.sh
|
||||||
|
|
||||||
cross-build:
|
cross-build:
|
||||||
name: cross-build
|
name: cross-build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
12
Makefile
12
Makefile
|
|
@ -1,10 +1,12 @@
|
||||||
APP=sshkeeper
|
APP=sshkeeper
|
||||||
|
VERSION ?= $(shell git describe --tags --match 'v*' --always --dirty 2>/dev/null || echo dev)
|
||||||
|
LDFLAGS = -s -w -X github.com/mirivlad/sshkeeper/cmd.Version=$(VERSION)
|
||||||
RELEASE_CHECK_DIR ?= /tmp/sshkeeper-release-check
|
RELEASE_CHECK_DIR ?= /tmp/sshkeeper-release-check
|
||||||
|
|
||||||
.PHONY: build run test vet fmt clean install release-check
|
.PHONY: build run test vet fmt clean install packaging-test release-check
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -o bin/$(APP) .
|
go build -ldflags "$(LDFLAGS)" -o bin/$(APP) .
|
||||||
|
|
||||||
run:
|
run:
|
||||||
go run .
|
go run .
|
||||||
|
|
@ -22,13 +24,17 @@ clean:
|
||||||
rm -rf bin
|
rm -rf bin
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go build -o $(HOME)/.local/bin/$(APP) .
|
go build -ldflags "$(LDFLAGS)" -o $(HOME)/.local/bin/$(APP) .
|
||||||
|
|
||||||
|
packaging-test:
|
||||||
|
./packaging/scripts/test-legacy-migration.sh
|
||||||
|
|
||||||
release-check:
|
release-check:
|
||||||
rm -rf $(RELEASE_CHECK_DIR)
|
rm -rf $(RELEASE_CHECK_DIR)
|
||||||
mkdir -p $(RELEASE_CHECK_DIR)
|
mkdir -p $(RELEASE_CHECK_DIR)
|
||||||
go test ./...
|
go test ./...
|
||||||
go vet ./...
|
go vet ./...
|
||||||
|
./packaging/scripts/test-legacy-migration.sh
|
||||||
CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP) .
|
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=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=linux GOARCH=arm64 CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP)-linux-arm64 .
|
||||||
|
|
|
||||||
25
README.md
25
README.md
|
|
@ -71,21 +71,34 @@ Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
|
||||||
Debian/Ubuntu (amd64):
|
Debian/Ubuntu (amd64):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install ./sshkeeper_0.4.0-1_amd64.deb
|
sudo apt install ./sshkeeper_0.4.1-1_amd64.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
Fedora/RHEL-family (x86_64):
|
Fedora/RHEL-family (x86_64):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dnf install ./sshkeeper-0.4.0-1.x86_64.rpm
|
sudo dnf install ./sshkeeper-0.4.1-1.x86_64.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
`arm64`/`aarch64` packages are published alongside the x86_64 builds. The
|
`arm64`/`aarch64` packages are published alongside the x86_64 builds. Native
|
||||||
traditional tar.gz archive remains available too:
|
packages own command resolution: when installing or upgrading they detect old
|
||||||
|
`/usr/local/bin/sshkeeper` and local-account `~/.local/bin/sshkeeper` copies, preserve
|
||||||
|
each as `*.legacy-backup`, and redirect the old path to `/usr/bin/sshkeeper`.
|
||||||
|
Removing the package restores preserved legacy binaries.
|
||||||
|
|
||||||
|
Check the exact running binary and embedded version with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar -xzf sshkeeper_v0.4.0_linux_amd64.tar.gz
|
command -v sshkeeper
|
||||||
sudo install -m 0755 sshkeeper_v0.4.0_linux_amd64/sshkeeper /usr/local/bin/sshkeeper
|
sshkeeper --version
|
||||||
|
# or: sshkeeper version
|
||||||
|
```
|
||||||
|
|
||||||
|
The traditional tar.gz archive remains available too:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar -xzf sshkeeper_v0.4.1_linux_amd64.tar.gz
|
||||||
|
sudo install -m 0755 sshkeeper_v0.4.1_linux_amd64/sshkeeper /usr/local/bin/sshkeeper
|
||||||
sshkeeper
|
sshkeeper
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
2
build.sh
2
build.sh
|
|
@ -8,7 +8,7 @@ APP=sshkeeper
|
||||||
# plain `git describe --tags` picks whichever tag is nearest, so a nightly build
|
# plain `git describe --tags` picks whichever tag is nearest, so a nightly build
|
||||||
# would otherwise stamp binaries "nightly" instead of v<last release>-N-g<sha>.
|
# would otherwise stamp binaries "nightly" instead of v<last release>-N-g<sha>.
|
||||||
VERSION=$(git describe --tags --match 'v*' --always --dirty 2>/dev/null || echo "dev")
|
VERSION=$(git describe --tags --match 'v*' --always --dirty 2>/dev/null || echo "dev")
|
||||||
LDFLAGS="-s -w -X main.version=${VERSION}"
|
LDFLAGS="-s -w -X github.com/mirivlad/sshkeeper/cmd.Version=${VERSION}"
|
||||||
|
|
||||||
echo "==> Building ${APP} ${VERSION}..."
|
echo "==> Building ${APP} ${VERSION}..."
|
||||||
go build -ldflags "${LDFLAGS}" -o bin/${APP} .
|
go build -ldflags "${LDFLAGS}" -o bin/${APP} .
|
||||||
|
|
|
||||||
19
cmd/root.go
19
cmd/root.go
|
|
@ -20,6 +20,7 @@ var (
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "sshkeeper",
|
Use: "sshkeeper",
|
||||||
|
Version: Version,
|
||||||
Short: "sshkeeper — SSH connection manager",
|
Short: "sshkeeper — SSH connection manager",
|
||||||
Long: `sshkeeper is a console SSH connection manager.
|
Long: `sshkeeper is a console SSH connection manager.
|
||||||
Linux and macOS are primary release targets; Windows is experimental.
|
Linux and macOS are primary release targets; Windows is experimental.
|
||||||
|
|
@ -38,7 +39,9 @@ func Execute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
rootCmd.SetVersionTemplate("sshkeeper {{.Version}}\n")
|
||||||
cobra.OnInitialize(initApp)
|
cobra.OnInitialize(initApp)
|
||||||
|
rootCmd.AddCommand(versionCmd)
|
||||||
rootCmd.AddCommand(initCmd)
|
rootCmd.AddCommand(initCmd)
|
||||||
rootCmd.AddCommand(addCmd)
|
rootCmd.AddCommand(addCmd)
|
||||||
rootCmd.AddCommand(listCmd)
|
rootCmd.AddCommand(listCmd)
|
||||||
|
|
@ -63,6 +66,10 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func initApp() {
|
func initApp() {
|
||||||
|
if commandSkipsAppInitialization(os.Args[1:]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
cfg, err = config.Load()
|
cfg, err = config.Load()
|
||||||
|
|
@ -171,6 +178,18 @@ func initApp() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func commandSkipsAppInitialization(args []string) bool {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == "-h" || arg == "--help" || arg == "--version" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args[0] == "version"
|
||||||
|
}
|
||||||
|
|
||||||
func commandRequiresStartupVaultUnlock(args []string) bool {
|
func commandRequiresStartupVaultUnlock(args []string) bool {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ func TestCommandRequiresStartupVaultUnlock(t *testing.T) {
|
||||||
{name: "background tunnel does not need startup vault", args: []string{"tunnel", "prod", "--background"}, want: false},
|
{name: "background tunnel does not need startup vault", args: []string{"tunnel", "prod", "--background"}, want: false},
|
||||||
{name: "config path only reads config", args: []string{"config", "path"}, want: false},
|
{name: "config path only reads config", args: []string{"config", "path"}, want: false},
|
||||||
{name: "help", args: []string{"--help"}, want: false},
|
{name: "help", args: []string{"--help"}, want: false},
|
||||||
|
{name: "version command", args: []string{"version"}, want: false},
|
||||||
|
{name: "version flag", args: []string{"--version"}, want: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
@ -34,3 +36,23 @@ func TestCommandRequiresStartupVaultUnlock(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommandSkipsAppInitialization(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
args []string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{args: nil, want: false},
|
||||||
|
{args: []string{"list"}, want: false},
|
||||||
|
{args: []string{"version"}, want: true},
|
||||||
|
{args: []string{"--version"}, want: true},
|
||||||
|
{args: []string{"--help"}, want: true},
|
||||||
|
{args: []string{"list", "--help"}, want: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := commandSkipsAppInitialization(tt.args); got != tt.want {
|
||||||
|
t.Fatalf("commandSkipsAppInitialization(%v) = %v; want %v", tt.args, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version is replaced at link time by build.sh/release.sh.
|
||||||
|
var Version = "dev"
|
||||||
|
|
||||||
|
func newVersionCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Print sshkeeper version",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
_, err := fmt.Fprintf(cmd.OutOrStdout(), "sshkeeper %s\n", Version)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionCmd = newVersionCmd()
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVersionCommand(t *testing.T) {
|
||||||
|
original := Version
|
||||||
|
Version = "v9.8.7-test"
|
||||||
|
t.Cleanup(func() { Version = original })
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd := newVersionCmd()
|
||||||
|
cmd.SetOut(&out)
|
||||||
|
if err := cmd.RunE(cmd, nil); err != nil {
|
||||||
|
t.Fatalf("version command: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := out.String(), "sshkeeper v9.8.7-test\n"; got != want {
|
||||||
|
t.Fatalf("output = %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootVersionFlagIsEnabled(t *testing.T) {
|
||||||
|
if rootCmd.Version == "" {
|
||||||
|
t.Fatal("root command Version must be set so Cobra exposes --version")
|
||||||
|
}
|
||||||
|
if !strings.Contains(rootCmd.Version, "v") && rootCmd.Version != "dev" {
|
||||||
|
t.Fatalf("unexpected root version %q", rootCmd.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -131,6 +131,14 @@ README, LICENSE, and the user guide under `/usr/share/doc/sshkeeper/`. Debian
|
||||||
packages depend on `openssh-client`; RPM packages depend on `openssh-clients`.
|
packages depend on `openssh-client`; RPM packages depend on `openssh-clients`.
|
||||||
The package revision starts at `1` and is reset when the upstream version changes.
|
The package revision starts at `1` and is reset when the upstream version changes.
|
||||||
|
|
||||||
|
Package maintainer scripts also migrate known legacy command paths. Existing
|
||||||
|
`/usr/local/bin/sshkeeper` and per-user `~/.local/bin/sshkeeper` entries are moved
|
||||||
|
to a non-destructive `*.legacy-backup` and replaced by symlinks to the packaged
|
||||||
|
`/usr/bin/sshkeeper`. This makes package installation authoritative even for a
|
||||||
|
shell that already cached the old command path. Home-directory changes are executed as the account owner rather than as root.
|
||||||
|
Package removal restores backups; package upgrades keep the redirect active. Migration behavior is covered by
|
||||||
|
`packaging/scripts/test-legacy-migration.sh` and is part of `make release-check`.
|
||||||
|
|
||||||
## Verify Checksums
|
## Verify Checksums
|
||||||
|
|
||||||
From the `dist/` directory:
|
From the `dist/` directory:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# sshkeeper v0.4.1 — Package Install & Version Fixes
|
||||||
|
|
||||||
|
This patch release makes native Linux packages authoritative after installation
|
||||||
|
and exposes the version embedded in the executable.
|
||||||
|
|
||||||
|
## Package installation now wins over legacy binaries
|
||||||
|
|
||||||
|
DEB and RPM installs detect older sshkeeper copies at known legacy paths:
|
||||||
|
|
||||||
|
- `/usr/local/bin/sshkeeper`;
|
||||||
|
- per-user `~/.local/bin/sshkeeper` paths discovered from the system account database.
|
||||||
|
|
||||||
|
A detected legacy entry is preserved as `*.legacy-backup`, then its old path is
|
||||||
|
replaced by a symlink to `/usr/bin/sshkeeper`. This deliberately handles both
|
||||||
|
`PATH` precedence and shells that have already cached the previous executable
|
||||||
|
path. No database, vault, configuration, SSH key, or other user data is touched.
|
||||||
|
|
||||||
|
The migration is idempotent across package upgrades. Removing the package restores
|
||||||
|
the preserved legacy binary when the redirect is still package-managed; if the
|
||||||
|
user changed that path while the package was installed, the package leaves the
|
||||||
|
user's replacement alone and keeps the backup rather than overwriting it.
|
||||||
|
|
||||||
|
## Version reporting
|
||||||
|
|
||||||
|
Both forms are now supported without initializing the database or vault:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sshkeeper --version
|
||||||
|
sshkeeper version
|
||||||
|
```
|
||||||
|
|
||||||
|
A v0.4.1 release binary prints:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sshkeeper v0.4.1
|
||||||
|
```
|
||||||
|
|
||||||
|
The build and release scripts now link the discovered version into the real
|
||||||
|
`cmd.Version` symbol instead of the stale `main.version` target.
|
||||||
|
|
||||||
|
## Packaging verification
|
||||||
|
|
||||||
|
The release gate now tests legacy-path migration and restoration in addition to
|
||||||
|
the existing Go tests, vet, and cross-platform builds. DEB/RPM packages continue
|
||||||
|
to contain the exact Linux binary produced for the matching tarball.
|
||||||
|
|
@ -39,9 +39,15 @@ overrides:
|
||||||
deb:
|
deb:
|
||||||
depends:
|
depends:
|
||||||
- openssh-client
|
- openssh-client
|
||||||
|
scripts:
|
||||||
|
postinstall: ./packaging/scripts/postinstall.sh
|
||||||
|
postremove: ./packaging/scripts/postremove.sh
|
||||||
rpm:
|
rpm:
|
||||||
depends:
|
depends:
|
||||||
- openssh-clients
|
- openssh-clients
|
||||||
|
scripts:
|
||||||
|
postinstall: ./packaging/scripts/postinstall.sh
|
||||||
|
postremove: ./packaging/scripts/postremove.sh
|
||||||
|
|
||||||
rpm:
|
rpm:
|
||||||
compression: gzip
|
compression: gzip
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -u
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
SYSTEM_BINARY=${SSHKEEPER_SYSTEM_BINARY:-/usr/bin/sshkeeper}
|
||||||
|
STATE_FILE=${SSHKEEPER_STATE_FILE:-/var/lib/sshkeeper/package-legacy-paths}
|
||||||
|
|
||||||
|
candidate_paths() {
|
||||||
|
if [ -n "${SSHKEEPER_LEGACY_PATHS:-}" ]; then
|
||||||
|
printf '%s\n' "$SSHKEEPER_LEGACY_PATHS" | while IFS= read -r path; do
|
||||||
|
printf 'test\t%s\n' "$path"
|
||||||
|
done
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'root\t%s\n' /usr/local/bin/sshkeeper
|
||||||
|
if [ -r /etc/passwd ]; then
|
||||||
|
awk -F: '$3 == 0 || $3 >= 1000 { if ($6 != "" && $6 != "/") printf "%s\\t%s/.local/bin/sshkeeper\\n", $1, $6 }' /etc/passwd
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_as() {
|
||||||
|
owner=$1
|
||||||
|
shift
|
||||||
|
if [ "${SSHKEEPER_MIGRATION_RUN_AS_CURRENT:-}" = 1 ]; then
|
||||||
|
"$@"
|
||||||
|
elif [ "$owner" = root ]; then
|
||||||
|
"$@"
|
||||||
|
elif command -v runuser >/dev/null 2>&1; then
|
||||||
|
runuser -u "$owner" -- "$@"
|
||||||
|
else
|
||||||
|
return 127
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
next_backup() {
|
||||||
|
path=$1
|
||||||
|
base="${path}.legacy-backup"
|
||||||
|
if [ ! -e "$base" ] && [ ! -L "$base" ]; then
|
||||||
|
printf '%s\n' "$base"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
n=1
|
||||||
|
while [ -e "${base}.${n}" ] || [ -L "${base}.${n}" ]; do
|
||||||
|
n=$((n + 1))
|
||||||
|
done
|
||||||
|
printf '%s\n' "${base}.${n}"
|
||||||
|
}
|
||||||
|
|
||||||
|
record_migration() {
|
||||||
|
owner=$1
|
||||||
|
path=$2
|
||||||
|
backup=$3
|
||||||
|
state_dir=$(dirname "$STATE_FILE")
|
||||||
|
if mkdir -p "$state_dir" 2>/dev/null; then
|
||||||
|
chmod 700 "$state_dir" 2>/dev/null || true
|
||||||
|
printf '%s\t%s\t%s\n' "$owner" "$path" "$backup" >> "$STATE_FILE"
|
||||||
|
chmod 600 "$STATE_FILE" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
printf 'sshkeeper: warning: cannot create migration state directory %s\n' "$state_dir" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate_one() {
|
||||||
|
owner=$1
|
||||||
|
path=$2
|
||||||
|
[ "$path" = "$SYSTEM_BINARY" ] && return 0
|
||||||
|
[ -e "$path" ] || [ -L "$path" ] || return 0
|
||||||
|
|
||||||
|
if [ -L "$path" ] && [ "$(readlink "$path" 2>/dev/null || true)" = "$SYSTEM_BINARY" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
backup=$(next_backup "$path")
|
||||||
|
if ! run_as "$owner" mv -- "$path" "$backup" 2>/dev/null; then
|
||||||
|
printf 'sshkeeper: warning: cannot disable legacy binary %s as user %s\n' "$path" "$owner" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! run_as "$owner" ln -s "$SYSTEM_BINARY" "$path" 2>/dev/null; then
|
||||||
|
run_as "$owner" mv -- "$backup" "$path" 2>/dev/null || true
|
||||||
|
printf 'sshkeeper: warning: cannot redirect legacy path %s to %s\n' "$path" "$SYSTEM_BINARY" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
record_migration "$owner" "$path" "$backup"
|
||||||
|
printf 'sshkeeper: migrated legacy binary: %s -> %s (backup: %s)\n' "$path" "$SYSTEM_BINARY" "$backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
tab=$(printf '\t')
|
||||||
|
candidate_paths | while IFS="$tab" read -r owner path; do
|
||||||
|
[ -n "$owner" ] && [ -n "$path" ] && migrate_one "$owner" "$path"
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -u
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
SYSTEM_BINARY=${SSHKEEPER_SYSTEM_BINARY:-/usr/bin/sshkeeper}
|
||||||
|
STATE_FILE=${SSHKEEPER_STATE_FILE:-/var/lib/sshkeeper/package-legacy-paths}
|
||||||
|
action=${1:-remove}
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
1|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
0|remove|purge)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -f "$STATE_FILE" ] || exit 0
|
||||||
|
|
||||||
|
run_as() {
|
||||||
|
owner=$1
|
||||||
|
shift
|
||||||
|
if [ "${SSHKEEPER_MIGRATION_RUN_AS_CURRENT:-}" = 1 ]; then
|
||||||
|
"$@"
|
||||||
|
elif [ "$owner" = root ]; then
|
||||||
|
"$@"
|
||||||
|
elif command -v runuser >/dev/null 2>&1; then
|
||||||
|
runuser -u "$owner" -- "$@"
|
||||||
|
else
|
||||||
|
return 127
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tab=$(printf '\t')
|
||||||
|
tac "$STATE_FILE" 2>/dev/null | while IFS="$tab" read -r owner path backup; do
|
||||||
|
[ -n "$owner" ] || continue
|
||||||
|
[ -n "$path" ] || continue
|
||||||
|
[ -n "$backup" ] || continue
|
||||||
|
|
||||||
|
managed=false
|
||||||
|
if [ -L "$path" ] && [ "$(readlink "$path" 2>/dev/null || true)" = "$SYSTEM_BINARY" ]; then
|
||||||
|
managed=true
|
||||||
|
run_as "$owner" rm -f -- "$path" 2>/dev/null || managed=false
|
||||||
|
elif [ ! -e "$path" ] && [ ! -L "$path" ]; then
|
||||||
|
managed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$managed" = true ] && { [ -e "$backup" ] || [ -L "$backup" ]; }; then
|
||||||
|
if run_as "$owner" mv -- "$backup" "$path" 2>/dev/null; then
|
||||||
|
printf 'sshkeeper: restored legacy binary: %s\n' "$path"
|
||||||
|
else
|
||||||
|
printf 'sshkeeper: warning: could not restore %s from %s\n' "$path" "$backup" >&2
|
||||||
|
fi
|
||||||
|
elif [ -e "$backup" ] || [ -L "$backup" ]; then
|
||||||
|
printf 'sshkeeper: warning: %s changed while package was installed; legacy backup kept at %s\n' "$path" "$backup" >&2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -f -- "$STATE_FILE" 2>/dev/null || true
|
||||||
|
rmdir -- "$(dirname "$STATE_FILE")" 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
|
system="$tmp/usr/bin/sshkeeper"
|
||||||
|
legacy_user="$tmp/home/test/.local/bin/sshkeeper"
|
||||||
|
legacy_local="$tmp/usr/local/bin/sshkeeper"
|
||||||
|
state="$tmp/var/lib/sshkeeper/package-legacy-paths"
|
||||||
|
mkdir -p "$(dirname "$system")" "$(dirname "$legacy_user")" "$(dirname "$legacy_local")"
|
||||||
|
printf 'packaged\n' > "$system"
|
||||||
|
printf 'old-user\n' > "$legacy_user"
|
||||||
|
printf 'old-local\n' > "$legacy_local"
|
||||||
|
chmod +x "$system" "$legacy_user" "$legacy_local"
|
||||||
|
|
||||||
|
paths=$(printf '%s\n%s' "$legacy_user" "$legacy_local")
|
||||||
|
env SSHKEEPER_MIGRATION_RUN_AS_CURRENT=1 SSHKEEPER_SYSTEM_BINARY="$system" SSHKEEPER_STATE_FILE="$state" SSHKEEPER_LEGACY_PATHS="$paths" \
|
||||||
|
packaging/scripts/postinstall.sh configure
|
||||||
|
|
||||||
|
for path in "$legacy_user" "$legacy_local"; do
|
||||||
|
test -L "$path"
|
||||||
|
test "$(readlink "$path")" = "$system"
|
||||||
|
test -f "${path}.legacy-backup"
|
||||||
|
done
|
||||||
|
test "$(wc -l < "$state")" -eq 2
|
||||||
|
test "$(stat -c %a "$state")" = 600
|
||||||
|
|
||||||
|
# Re-running postinstall on upgrade is idempotent.
|
||||||
|
env SSHKEEPER_MIGRATION_RUN_AS_CURRENT=1 SSHKEEPER_SYSTEM_BINARY="$system" SSHKEEPER_STATE_FILE="$state" SSHKEEPER_LEGACY_PATHS="$paths" \
|
||||||
|
packaging/scripts/postinstall.sh configure 0.4.0
|
||||||
|
test "$(wc -l < "$state")" -eq 2
|
||||||
|
|
||||||
|
env SSHKEEPER_MIGRATION_RUN_AS_CURRENT=1 SSHKEEPER_SYSTEM_BINARY="$system" SSHKEEPER_STATE_FILE="$state" \
|
||||||
|
packaging/scripts/postremove.sh upgrade
|
||||||
|
for path in "$legacy_user" "$legacy_local"; do test -L "$path"; done
|
||||||
|
|
||||||
|
env SSHKEEPER_MIGRATION_RUN_AS_CURRENT=1 SSHKEEPER_SYSTEM_BINARY="$system" SSHKEEPER_STATE_FILE="$state" \
|
||||||
|
packaging/scripts/postremove.sh remove
|
||||||
|
|
||||||
|
test ! -e "$state"
|
||||||
|
test ! -L "$legacy_user"
|
||||||
|
test ! -L "$legacy_local"
|
||||||
|
test "$(cat "$legacy_user")" = old-user
|
||||||
|
test "$(cat "$legacy_local")" = old-local
|
||||||
|
|
||||||
|
# If a user replaces the package-managed redirect, uninstall must not overwrite it.
|
||||||
|
printf 'old-again\n' > "$legacy_user"
|
||||||
|
env SSHKEEPER_MIGRATION_RUN_AS_CURRENT=1 SSHKEEPER_SYSTEM_BINARY="$system" SSHKEEPER_STATE_FILE="$state" SSHKEEPER_LEGACY_PATHS="$legacy_user" \
|
||||||
|
packaging/scripts/postinstall.sh configure
|
||||||
|
rm -f "$legacy_user"
|
||||||
|
printf 'user-replacement\n' > "$legacy_user"
|
||||||
|
env SSHKEEPER_MIGRATION_RUN_AS_CURRENT=1 SSHKEEPER_SYSTEM_BINARY="$system" SSHKEEPER_STATE_FILE="$state" \
|
||||||
|
packaging/scripts/postremove.sh remove
|
||||||
|
test "$(cat "$legacy_user")" = user-replacement
|
||||||
|
test "$(cat "${legacy_user}.legacy-backup")" = old-again
|
||||||
|
|
||||||
|
echo "legacy migration tests: OK"
|
||||||
|
|
@ -6,7 +6,7 @@ cd "$(dirname "$0")"
|
||||||
APP=sshkeeper
|
APP=sshkeeper
|
||||||
# --match 'v*' ignores the rolling `nightly` tag; see build.sh for the details.
|
# --match 'v*' ignores the rolling `nightly` tag; see build.sh for the details.
|
||||||
VERSION=${VERSION:-${1:-$(git describe --tags --match 'v*' --always --dirty 2>/dev/null || echo "dev")}}
|
VERSION=${VERSION:-${1:-$(git describe --tags --match 'v*' --always --dirty 2>/dev/null || echo "dev")}}
|
||||||
LDFLAGS="-s -w -X main.version=${VERSION}"
|
LDFLAGS="-s -w -X github.com/mirivlad/sshkeeper/cmd.Version=${VERSION}"
|
||||||
DIST_DIR="dist"
|
DIST_DIR="dist"
|
||||||
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-$(git log -1 --format=%ct 2>/dev/null || date +%s)}
|
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-$(git log -1 --format=%ct 2>/dev/null || date +%s)}
|
||||||
export SOURCE_DATE_EPOCH
|
export SOURCE_DATE_EPOCH
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue