repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

commit
57bb60c
parent
64dea57
author
Eric Bower
date
2025-02-17 10:36:03 -0500 EST
chore: add `rsync --delete` test

I can't figure out how to format the `exec.Command` for rsync with `-e`
flag

Current error:

```
[rsync -rv -e "ssh -p 2222 -o IdentitiesOnly=yes -i /tmp/rsync-3246995037/id_ed25519 -o StrictHostKeyChecking=no" /tmp/rsync-3246995037/ localhost:/test]
rsync: [sender] Failed to exec ssh -p 2222 -o IdentitiesOnly=yes -i /tmp/rsync-3246995037/id_ed25519 -o StrictHostKeyChecking=no: No such file or directory (2)
rsync error: error in IPC code (code 14) at pipe.c(85) [sender=3.4.0]
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in IPC code (code 14) at io.c(232) [sender=3.4.0]
 exit status 14
--- FAIL: TestSshServerRsync (0.10s)
    ssh_test.go:152: exit status 14
```
1 files changed,  +149, -5
M pgs/ssh_test.go
+149, -5
  1@@ -3,9 +3,14 @@ package pgs
  2 import (
  3 	"crypto/ed25519"
  4 	"crypto/rand"
  5+	"crypto/x509"
  6+	"encoding/pem"
  7+	"fmt"
  8 	"io"
  9 	"log/slog"
 10 	"os"
 11+	"os/exec"
 12+	"path/filepath"
 13 	"strings"
 14 	"testing"
 15 	"time"
 16@@ -18,7 +23,7 @@ import (
 17 	"golang.org/x/crypto/ssh"
 18 )
 19 
 20-func TestSshServer(t *testing.T) {
 21+func TestSshServerSftp(t *testing.T) {
 22 	logger := slog.Default()
 23 	dbpool := pgsdb.NewDBMemory(logger)
 24 	// setup test data
 25@@ -57,9 +62,142 @@ func TestSshServer(t *testing.T) {
 26 	done <- nil
 27 }
 28 
 29+func TestSshServerRsync(t *testing.T) {
 30+	logger := slog.Default()
 31+	dbpool := pgsdb.NewDBMemory(logger)
 32+	// setup test data
 33+	dbpool.SetupTestData()
 34+	st, err := storage.NewStorageMemory(map[string]map[string]string{})
 35+	if err != nil {
 36+		panic(err)
 37+	}
 38+	cfg := NewPgsConfig(logger, dbpool, st)
 39+	done := make(chan error)
 40+	go StartSshServer(cfg, done)
 41+	// Hack to wait for startup
 42+	time.Sleep(time.Millisecond * 100)
 43+
 44+	user := GenerateUser()
 45+	key := utils.KeyForKeyText(user.signer.PublicKey())
 46+	// add user's pubkey to the default test account
 47+	dbpool.Pubkeys = append(dbpool.Pubkeys, &db.PublicKey{
 48+		ID:     "nice-pubkey",
 49+		UserID: dbpool.Users[0].ID,
 50+		Key:    key,
 51+	})
 52+
 53+	conn, err := user.NewClient()
 54+	if err != nil {
 55+		t.Error(err)
 56+		return
 57+	}
 58+	defer conn.Close()
 59+
 60+	// open an SFTP session over an existing ssh connection.
 61+	client, err := sftp.NewClient(conn)
 62+	if err != nil {
 63+		cfg.Logger.Error("could not create sftp client", "err", err)
 64+		panic(err)
 65+	}
 66+	defer client.Close()
 67+
 68+	name, err := os.MkdirTemp("", "rsync-")
 69+	if err != nil {
 70+		panic(err)
 71+	}
 72+
 73+	// remove the temporary directory at the end of the program
 74+	defer os.RemoveAll(name)
 75+
 76+	block := &pem.Block{
 77+		Type:  "PRIVATE KEY",
 78+		Bytes: user.privateKey,
 79+	}
 80+	keyFile := filepath.Join(name, "id_ed25519")
 81+	err = os.WriteFile(
 82+		keyFile,
 83+		pem.EncodeToMemory(block), 0600,
 84+	)
 85+
 86+	index := "<!doctype html><html><body>index</body></html>"
 87+	err = os.WriteFile(
 88+		filepath.Join(name, "index.html"),
 89+		[]byte(index), 0666,
 90+	)
 91+
 92+	about := "<!doctype html><html><body>about</body></html>"
 93+	aboutFile := filepath.Join(name, "about.html")
 94+	err = os.WriteFile(
 95+		aboutFile,
 96+		[]byte(about), 0666,
 97+	)
 98+
 99+	contact := "<!doctype html><html><body>contact</body></html>"
100+	err = os.WriteFile(
101+		filepath.Join(name, "contact.html"),
102+		[]byte(contact), 0666,
103+	)
104+
105+	eCmd := fmt.Sprintf(
106+		`"ssh -p 2222 -o IdentitiesOnly=yes -i %s -o StrictHostKeyChecking=no"`,
107+		keyFile,
108+	)
109+
110+	// copy files
111+	cmd := exec.Command("rsync", "-rv", "-e", eCmd, name+"/", "localhost:/test")
112+	fmt.Println(cmd.Args)
113+	result, err := cmd.CombinedOutput()
114+	if err != nil {
115+		fmt.Println(string(result), err)
116+		t.Error(err)
117+		return
118+	}
119+
120+	// check it's there
121+	fi, err := client.Lstat("about.html")
122+	if err != nil {
123+		cfg.Logger.Error("could not get stat for file", "err", err)
124+		t.Error("about.html not found")
125+		return
126+	}
127+	if fi.Size() != 0 {
128+		cfg.Logger.Error("about.html wrong size", "size", fi.Size())
129+		t.Error("about.html wrong size")
130+		return
131+	}
132+
133+	// remove about file
134+	os.Remove(aboutFile)
135+
136+	// copy files with delete
137+	delCmd := exec.Command("rsync", "-rv", "--delete", "-e", eCmd, name+"/", "localhost:/test")
138+	err = delCmd.Run()
139+	if err != nil {
140+		t.Error(err)
141+		return
142+	}
143+
144+	done <- nil
145+}
146+
147+func createTmpFile(name, contents, ext string) *os.File {
148+	file, err := os.CreateTemp("tmp", fmt.Sprintf("%s-*.%s", name, ext))
149+	if err != nil {
150+		panic(err)
151+	}
152+
153+	data := []byte(contents)
154+	if _, err := file.Write(data); err != nil {
155+		panic(err)
156+	}
157+
158+	return file
159+}
160+
161 type UserSSH struct {
162-	username string
163-	signer   ssh.Signer
164+	username   string
165+	signer     ssh.Signer
166+	privateKey []byte
167 }
168 
169 func NewUserSSH(username string, signer ssh.Signer) *UserSSH {
170@@ -146,14 +284,20 @@ func GenerateUser() UserSSH {
171 		panic(err)
172 	}
173 
174+	b, err := x509.MarshalPKCS8PrivateKey(userKey)
175+	if err != nil {
176+		panic(err)
177+	}
178+
179 	userSigner, err := ssh.NewSignerFromKey(userKey)
180 	if err != nil {
181 		panic(err)
182 	}
183 
184 	return UserSSH{
185-		username: "testuser",
186-		signer:   userSigner,
187+		username:   "testuser",
188+		signer:     userSigner,
189+		privateKey: b,
190 	}
191 }
192