Skip to content

Commit 1f44ff6

Browse files
committed
Add support for removing configs with associated SSH keys
1 parent a976da7 commit 1f44ff6

4 files changed

Lines changed: 117 additions & 6 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ sshc edit config my-server --user new-admin --host new-example.com
7575

7676
# Remove a managed config
7777
sshc rm config my-server
78+
79+
# Remove a config and its associated SSH key
80+
sshc rm config my-server --delete-key
7881
```
7982

8083
## Examples

internal/cmd/rm.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import (
88
"github.com/spf13/cobra"
99
)
1010

11+
var (
12+
deleteKey bool
13+
)
14+
1115
var rmCmd = &cobra.Command{
1216
Use: "rm",
1317
Short: "Remove SSH configuration or keys",
@@ -24,16 +28,30 @@ var rmConfigCmd = &cobra.Command{
2428
return err
2529
}
2630

27-
if err := m.RemoveConfig(name); err != nil {
31+
idFile, err := m.RemoveConfigWithKey(name, deleteKey)
32+
if err != nil {
2833
return err
2934
}
3035

31-
fmt.Printf("Config %s removed successfully\n", name)
36+
if deleteKey {
37+
if idFile != "" {
38+
fmt.Printf("Config %s and its key %s removed successfully\n", name, idFile)
39+
} else {
40+
fmt.Printf("Config %s removed successfully (no identity file found to delete)\n", name)
41+
}
42+
} else {
43+
fmt.Printf("Config %s removed successfully\n", name)
44+
if idFile != "" {
45+
fmt.Printf("Warning: SSH key %s was not deleted. Use --delete-key to remove it.\n", idFile)
46+
}
47+
}
48+
3249
return nil
3350
},
3451
}
3552

3653
func init() {
54+
rmConfigCmd.Flags().BoolVar(&deleteKey, "delete-key", false, "Delete the associated SSH key")
3755
rmCmd.AddCommand(rmConfigCmd)
3856
rootCmd.AddCommand(rmCmd)
3957
}

internal/config/manager.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,41 @@ func (m *Manager) AddConfig(name string, content string) error {
9393
}
9494

9595
func (m *Manager) RemoveConfig(name string) error {
96+
_, err := m.RemoveConfigWithKey(name, false)
97+
return err
98+
}
99+
100+
func (m *Manager) RemoveConfigWithKey(name string, deleteKey bool) (string, error) {
96101
configPath := m.GetConfigPath(name)
97-
if err := os.Remove(configPath); err != nil {
102+
content, err := os.ReadFile(configPath)
103+
if err != nil {
98104
if os.IsNotExist(err) {
99-
return fmt.Errorf("config %s does not exist", name)
105+
return "", fmt.Errorf("config %s does not exist", name)
100106
}
101-
return fmt.Errorf("failed to remove config %s: %w", name, err)
107+
return "", fmt.Errorf("failed to read config %s: %w", name, err)
102108
}
103-
return nil
109+
110+
var identityFile string
111+
for line := range strings.SplitSeq(string(content), "\n") {
112+
trimmed := strings.TrimSpace(line)
113+
if strings.HasPrefix(strings.ToLower(trimmed), "identityfile ") {
114+
identityFile = strings.TrimSpace(trimmed[len("identityfile "):])
115+
break
116+
}
117+
}
118+
119+
if err := os.Remove(configPath); err != nil {
120+
return "", fmt.Errorf("failed to remove config %s: %w", name, err)
121+
}
122+
123+
if deleteKey && identityFile != "" {
124+
// Try to remove the private key
125+
_ = os.Remove(identityFile)
126+
// Try to remove the public key
127+
_ = os.Remove(identityFile + ".pub")
128+
}
129+
130+
return identityFile, nil
104131
}
105132

106133
type ConfigOptions struct {

internal/config/manager_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,69 @@ func TestManager_AddRemoveConfig(t *testing.T) {
150150
}
151151
}
152152

153+
func TestManager_RemoveConfig_WithKey(t *testing.T) {
154+
tmpDir, err := os.MkdirTemp("", "sshc-test-rm-key")
155+
if err != nil {
156+
t.Fatal(err)
157+
}
158+
defer os.RemoveAll(tmpDir)
159+
160+
m := &Manager{
161+
SshDir: tmpDir,
162+
}
163+
_ = m.Init()
164+
165+
name := "key-config"
166+
keyPath := filepath.Join(tmpDir, "test-key")
167+
content := "Host test\n IdentityFile " + keyPath
168+
169+
// Create dummy key files
170+
if err := os.WriteFile(keyPath, []byte("private"), 0600); err != nil {
171+
t.Fatal(err)
172+
}
173+
if err := os.WriteFile(keyPath+".pub", []byte("public"), 0644); err != nil {
174+
t.Fatal(err)
175+
}
176+
177+
if err := m.AddConfig(name, content); err != nil {
178+
t.Fatal(err)
179+
}
180+
181+
// 1. Remove WITHOUT deleting key
182+
idFile, err := m.RemoveConfigWithKey(name, false)
183+
if err != nil {
184+
t.Errorf("RemoveConfigWithKey(false) error = %v", err)
185+
}
186+
if idFile != keyPath {
187+
t.Errorf("Expected idFile %s, got %s", keyPath, idFile)
188+
}
189+
190+
// Verify key still exists
191+
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
192+
t.Errorf("Key should still exist")
193+
}
194+
195+
// 2. Add back and remove WITH deleting key
196+
if err := m.AddConfig(name, content); err != nil {
197+
t.Fatal(err)
198+
}
199+
idFile, err = m.RemoveConfigWithKey(name, true)
200+
if err != nil {
201+
t.Errorf("RemoveConfigWithKey(true) error = %v", err)
202+
}
203+
if idFile != keyPath {
204+
t.Errorf("Expected idFile %s, got %s", keyPath, idFile)
205+
}
206+
207+
// Verify key is deleted
208+
if _, err := os.Stat(keyPath); !os.IsNotExist(err) {
209+
t.Errorf("Key should be deleted")
210+
}
211+
if _, err := os.Stat(keyPath + ".pub"); !os.IsNotExist(err) {
212+
t.Errorf("Public key should be deleted")
213+
}
214+
}
215+
153216
func TestManager_ListConfigs_Sorted(t *testing.T) {
154217
tmpDir, err := os.MkdirTemp("", "sshc-test-list")
155218
if err != nil {

0 commit comments

Comments
 (0)