Skip to content

Commit 66029d5

Browse files
committed
fix: keep SSH known_hosts temp file alive until repository cleanup
The known_hosts temp file was deleted via defer in getSSHClientOptions, which runs before the SSH connection is established in CloneRepository. This caused host key verification to fail silently when known_hosts data was provided. Move temp file lifecycle management to Repository, which cleans up all temp files in its Cleanup method alongside the cloned directory.
1 parent 0bcc947 commit 66029d5

3 files changed

Lines changed: 41 additions & 22 deletions

File tree

internal/git/manager.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ func (m *managerImpl) CloneRepository(ctx context.Context, repoUrl, subPath, ref
5454
return nil, fmt.Errorf("failed to create temporary directory: %w", err)
5555
}
5656

57+
clientOpts, tempFile := m.getClientOptions(parsedURL.Scheme, auth)
58+
5759
repo, err := git.PlainCloneContext(ctx, targetDir, &git.CloneOptions{
5860
URL: repoUrl,
5961
ReferenceName: plumbing.ReferenceName(reference),
6062
SingleBranch: true,
6163
Depth: 1,
62-
ClientOptions: m.getClientOptions(parsedURL.Scheme, auth),
64+
ClientOptions: clientOpts,
6365
})
6466
if err != nil {
6567
return nil, fmt.Errorf("failed to clone repo: %w", err)
@@ -70,19 +72,22 @@ func (m *managerImpl) CloneRepository(ctx context.Context, repoUrl, subPath, ref
7072
return nil, fmt.Errorf("failed to find head: %w", err)
7173
}
7274

73-
return &Repository{
75+
result := &Repository{
7476
CloneDir: targetDir,
7577
SubPath: subPath,
7678
Commit: head.Hash().String(),
7779
Branch: reference,
78-
}, nil
80+
}
81+
result.AddTempFile(tempFile)
82+
83+
return result, nil
7984
}
8085

81-
func (m *managerImpl) getClientOptions(scheme string, authSecret map[string][]byte) []client.Option {
86+
func (m *managerImpl) getClientOptions(scheme string, authSecret map[string][]byte) ([]client.Option, string) {
8287
if scheme == "ssh" {
8388
return m.getSSHClientOptions(authSecret)
8489
}
85-
return m.getHTTPClientOptions(authSecret)
90+
return m.getHTTPClientOptions(authSecret), ""
8691
}
8792

8893
func (m *managerImpl) getHTTPClientOptions(authSecret map[string][]byte) []client.Option {
@@ -125,37 +130,38 @@ func ensureKnownHostsExists() error {
125130
return nil
126131
}
127132

128-
func (m *managerImpl) getSSHClientOptions(authSecret map[string][]byte) []client.Option {
133+
func (m *managerImpl) getSSHClientOptions(authSecret map[string][]byte) ([]client.Option, string) {
129134
privateKey, hasKey := authSecret["sshPrivateKey"]
130135
if !hasKey {
131136
return []client.Option{
132137
client.WithSSHAuth(&gitssh.Password{
133138
User: "git",
134139
HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{HostKeyCallback: gossh.InsecureIgnoreHostKey()},
135140
}),
136-
}
141+
}, ""
137142
}
138143

139144
password := string(authSecret["sshPrivateKeyPassword"])
140145
auth, err := gitssh.NewPublicKeys("git", privateKey, password)
141146
if err != nil {
142-
return nil
147+
return nil, ""
143148
}
144149
auth.HostKeyCallback = gossh.InsecureIgnoreHostKey()
145150

151+
var tempFilePath string
146152
if knownHostsData, ok := authSecret["known_hosts"]; ok {
147153
tmpFile, err := os.CreateTemp("", "known_hosts-*")
148154
if err == nil {
149-
defer os.Remove(tmpFile.Name())
150155
if _, err := tmpFile.Write(knownHostsData); err == nil {
151156
_ = tmpFile.Close()
152-
cb, err := gitssh.NewKnownHostsCallback(tmpFile.Name())
157+
tempFilePath = tmpFile.Name()
158+
cb, err := gitssh.NewKnownHostsCallback(tempFilePath)
153159
if err == nil {
154160
auth.HostKeyCallback = cb
155161
}
156162
}
157163
}
158164
}
159165

160-
return []client.Option{client.WithSSHAuth(auth)}
166+
return []client.Option{client.WithSSHAuth(auth)}, tempFilePath
161167
}

internal/git/manager_test.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,40 +20,43 @@ JtdGRlLmNzYgECAw==
2020
func TestGetClientOptions_HTTPToken(t *testing.T) {
2121
m := &managerImpl{}
2222
secret := map[string][]byte{"token": []byte("my-token")}
23-
opts := m.getClientOptions("https", secret)
23+
opts, tmpFile := m.getClientOptions("https", secret)
2424
if len(opts) != 1 {
2525
t.Fatalf("expected 1 option, got %d", len(opts))
2626
}
27+
if tmpFile != "" {
28+
t.Fatalf("expected no temp file, got %s", tmpFile)
29+
}
2730
}
2831

2932
func TestGetClientOptions_HTTPUsernamePassword(t *testing.T) {
3033
m := &managerImpl{}
3134
secret := map[string][]byte{"username": []byte("user"), "password": []byte("pass")}
32-
opts := m.getClientOptions("http", secret)
35+
opts, _ := m.getClientOptions("http", secret)
3336
if len(opts) != 1 {
3437
t.Fatalf("expected 1 option, got %d", len(opts))
3538
}
3639
}
3740

3841
func TestGetClientOptions_HTTPEmpty(t *testing.T) {
3942
m := &managerImpl{}
40-
opts := m.getClientOptions("https", nil)
43+
opts, _ := m.getClientOptions("https", nil)
4144
if opts != nil {
4245
t.Fatalf("expected nil options, got %v", opts)
4346
}
4447
}
4548

4649
func TestGetClientOptions_SSHNoSecret(t *testing.T) {
4750
m := &managerImpl{}
48-
opts := m.getClientOptions(sshScheme, nil)
51+
opts, _ := m.getClientOptions(sshScheme, nil)
4952
if len(opts) != 1 {
5053
t.Fatalf("expected 1 option for insecure SSH, got %d", len(opts))
5154
}
5255
}
5356

5457
func TestGetClientOptions_SSHEmptySecret(t *testing.T) {
5558
m := &managerImpl{}
56-
opts := m.getClientOptions(sshScheme, map[string][]byte{})
59+
opts, _ := m.getClientOptions(sshScheme, map[string][]byte{})
5760
if len(opts) != 1 {
5861
t.Fatalf("expected 1 option for insecure SSH, got %d", len(opts))
5962
}
@@ -62,7 +65,7 @@ func TestGetClientOptions_SSHEmptySecret(t *testing.T) {
6265
func TestGetClientOptions_SSHWithPrivateKey(t *testing.T) {
6366
m := &managerImpl{}
6467
secret := map[string][]byte{"sshPrivateKey": []byte(testEd25519PrivateKey)}
65-
opts := m.getClientOptions(sshScheme, secret)
68+
opts, _ := m.getClientOptions(sshScheme, secret)
6669
if len(opts) != 1 {
6770
t.Fatalf("expected 1 option, got %d", len(opts))
6871
}
@@ -71,7 +74,7 @@ func TestGetClientOptions_SSHWithPrivateKey(t *testing.T) {
7174
func TestGetClientOptions_SSHWithInvalidKey(t *testing.T) {
7275
m := &managerImpl{}
7376
secret := map[string][]byte{"sshPrivateKey": []byte("not-a-valid-key")}
74-
opts := m.getClientOptions(sshScheme, secret)
77+
opts, _ := m.getClientOptions(sshScheme, secret)
7578
if opts != nil {
7679
t.Fatalf("expected nil options for invalid key, got %v", opts)
7780
}

internal/git/repository.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,26 @@ import (
66
)
77

88
type Repository struct {
9-
CloneDir string
10-
SubPath string
11-
Commit string
12-
Branch string
9+
CloneDir string
10+
SubPath string
11+
Commit string
12+
Branch string
13+
tempFiles []string
14+
}
15+
16+
func (r *Repository) AddTempFile(path string) {
17+
if path != "" {
18+
r.tempFiles = append(r.tempFiles, path)
19+
}
1320
}
1421

1522
func (r *Repository) Path() string {
1623
return path.Join(r.CloneDir, r.SubPath)
1724
}
1825

1926
func (r *Repository) Cleanup() error {
27+
for _, f := range r.tempFiles {
28+
os.Remove(f)
29+
}
2030
return os.RemoveAll(r.CloneDir)
2131
}

0 commit comments

Comments
 (0)