From 2030e766f18bfcbba52ab7008f78c29b90718414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 29 Apr 2026 17:28:46 +0200 Subject: [PATCH 01/10] feat: add SSH URL support to git manager Switch URL parsing to go-git's transport.ParseURL() which handles SCP-style (git@host:path), ssh://, http://, and https:// URLs. Add SSH auth support with private key, optional passphrase, and optional known_hosts verification. Simplify temp directory naming. --- go.mod | 2 +- internal/git/manager.go | 65 +++++++++++++++++--- internal/git/manager_test.go | 116 +++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 internal/git/manager_test.go diff --git a/go.mod b/go.mod index dd98a90..6dbc811 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/onsi/gomega v1.40.0 github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.50.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.35.4 k8s.io/apimachinery v0.35.4 @@ -122,7 +123,6 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.50.0 // indirect golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.53.0 // indirect diff --git a/internal/git/manager.go b/internal/git/manager.go index 9ad9645..05e751f 100644 --- a/internal/git/manager.go +++ b/internal/git/manager.go @@ -3,16 +3,18 @@ package git import ( "context" "fmt" - neturl "net/url" "os" - "strings" "github.com/functions-dev/func-operator/internal/monitoring" "github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/client" + "github.com/go-git/go-git/v6/plumbing/transport" "github.com/go-git/go-git/v6/plumbing/transport/http" + "github.com/go-git/go-git/v6/plumbing/transport/ssh" + "github.com/go-git/go-git/v6/plumbing/transport/ssh/knownhosts" "github.com/prometheus/client_golang/prometheus" + gossh "golang.org/x/crypto/ssh" ) const ( @@ -36,13 +38,12 @@ func (m *managerImpl) CloneRepository(ctx context.Context, repoUrl, subPath, ref timer := prometheus.NewTimer(monitoring.GitCloneDuration) defer timer.ObserveDuration() - url, err := neturl.Parse(repoUrl) + parsedURL, err := transport.ParseURL(repoUrl) if err != nil { return nil, fmt.Errorf("failed to parse repository URL: %w", err) } - pattern := fmt.Sprintf("%s-%s-%s", url.Host, strings.ReplaceAll(strings.TrimSuffix(url.Path, ".git"), "/", "-"), reference) - targetDir, err := os.MkdirTemp(cloneBaseDir, pattern) + targetDir, err := os.MkdirTemp(cloneBaseDir, "repo-*") if err != nil { return nil, fmt.Errorf("failed to create temporary directory: %w", err) } @@ -52,7 +53,7 @@ func (m *managerImpl) CloneRepository(ctx context.Context, repoUrl, subPath, ref ReferenceName: plumbing.ReferenceName(reference), SingleBranch: true, Depth: 1, - ClientOptions: m.getClientOptions(auth), + ClientOptions: m.getClientOptions(parsedURL.Scheme, auth), }) if err != nil { return nil, fmt.Errorf("failed to clone repo: %w", err) @@ -71,7 +72,14 @@ func (m *managerImpl) CloneRepository(ctx context.Context, repoUrl, subPath, ref }, nil } -func (m *managerImpl) getClientOptions(authSecret map[string][]byte) []client.Option { +func (m *managerImpl) getClientOptions(scheme string, authSecret map[string][]byte) []client.Option { + if scheme == "ssh" { + return m.getSSHClientOptions(authSecret) + } + return m.getHTTPClientOptions(authSecret) +} + +func (m *managerImpl) getHTTPClientOptions(authSecret map[string][]byte) []client.Option { if len(authSecret) == 0 { return nil } else if token, ok := authSecret["token"]; ok { @@ -91,6 +99,47 @@ func (m *managerImpl) getClientOptions(authSecret map[string][]byte) []client.Op } } return nil - } // add other auth methods when needed + } return nil } + +func (m *managerImpl) getSSHClientOptions(authSecret map[string][]byte) []client.Option { + privateKey, hasKey := authSecret["sshPrivateKey"] + if !hasKey { + return []client.Option{ + client.WithSSHAuth(&ssh.PublicKeys{ + User: "git", + HostKeyCallbackHelper: ssh.HostKeyCallbackHelper{ + HostKeyCallback: gossh.InsecureIgnoreHostKey(), + }, + }), + } + } + + password := string(authSecret["sshPrivateKeyPassword"]) + + auth, err := ssh.NewPublicKeys("git", privateKey, password) + if err != nil { + return nil + } + + if knownHostsData, ok := authSecret["known_hosts"]; ok { + tmpFile, err := os.CreateTemp("", "known_hosts-*") + if err == nil { + defer os.Remove(tmpFile.Name()) + if _, err := tmpFile.Write(knownHostsData); err == nil { + tmpFile.Close() + db, err := knownhosts.NewDB(tmpFile.Name()) + if err == nil { + auth.HostKeyCallback = db.HostKeyCallback() + } + } + } + } else { + auth.HostKeyCallback = gossh.InsecureIgnoreHostKey() + } + + return []client.Option{ + client.WithSSHAuth(auth), + } +} diff --git a/internal/git/manager_test.go b/internal/git/manager_test.go new file mode 100644 index 0000000..9f39f09 --- /dev/null +++ b/internal/git/manager_test.go @@ -0,0 +1,116 @@ +package git + +import ( + "testing" + + "github.com/go-git/go-git/v6/plumbing/transport" +) + +const testEd25519PrivateKey = `-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDAtq/Kt1/J1J/YivGDJIO57fFW1v68f1eq1N1Vr77BLAAAALB+/pd5fv6X +eQAAAAtzc2gtZWQyNTUxOQAAACDAtq/Kt1/J1J/YivGDJIO57fFW1v68f1eq1N1Vr77BLA +AAAEDDodLIs7cKTLW+FFH5jgfGo2b2iae1w5lbsIXiu8UZKcC2r8q3X8nUn9iK8YMkg7nt +8VbW/rx/V6rU3VWvvsEsAAAAKmNzdGFibGVyQGNzdGFibGVyLXRoaW5rcGFkcDFnZW43Ln +JtdGRlLmNzYgECAw== +-----END OPENSSH PRIVATE KEY-----` + +func TestGetClientOptions_HTTPToken(t *testing.T) { + m := &managerImpl{} + secret := map[string][]byte{"token": []byte("my-token")} + opts := m.getClientOptions("https", secret) + if len(opts) != 1 { + t.Fatalf("expected 1 option, got %d", len(opts)) + } +} + +func TestGetClientOptions_HTTPUsernamePassword(t *testing.T) { + m := &managerImpl{} + secret := map[string][]byte{"username": []byte("user"), "password": []byte("pass")} + opts := m.getClientOptions("http", secret) + if len(opts) != 1 { + t.Fatalf("expected 1 option, got %d", len(opts)) + } +} + +func TestGetClientOptions_HTTPEmpty(t *testing.T) { + m := &managerImpl{} + opts := m.getClientOptions("https", nil) + if opts != nil { + t.Fatalf("expected nil options, got %v", opts) + } +} + +func TestGetClientOptions_SSHNoSecret(t *testing.T) { + m := &managerImpl{} + opts := m.getClientOptions("ssh", nil) + if len(opts) != 1 { + t.Fatalf("expected 1 option for insecure SSH, got %d", len(opts)) + } +} + +func TestGetClientOptions_SSHEmptySecret(t *testing.T) { + m := &managerImpl{} + opts := m.getClientOptions("ssh", map[string][]byte{}) + if len(opts) != 1 { + t.Fatalf("expected 1 option for insecure SSH, got %d", len(opts)) + } +} + +func TestGetClientOptions_SSHWithPrivateKey(t *testing.T) { + m := &managerImpl{} + secret := map[string][]byte{"sshPrivateKey": []byte(testEd25519PrivateKey)} + opts := m.getClientOptions("ssh", secret) + if len(opts) != 1 { + t.Fatalf("expected 1 option, got %d", len(opts)) + } +} + +func TestGetClientOptions_SSHWithInvalidKey(t *testing.T) { + m := &managerImpl{} + secret := map[string][]byte{"sshPrivateKey": []byte("not-a-valid-key")} + opts := m.getClientOptions("ssh", secret) + if opts != nil { + t.Fatalf("expected nil options for invalid key, got %v", opts) + } +} + +func TestParseURL_SCPStyle(t *testing.T) { + u, err := transport.ParseURL("git@github.com:owner/repo.git") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if u.Scheme != "ssh" { + t.Fatalf("expected scheme ssh, got %s", u.Scheme) + } +} + +func TestParseURL_SSHScheme(t *testing.T) { + u, err := transport.ParseURL("ssh://git@github.com/owner/repo.git") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if u.Scheme != "ssh" { + t.Fatalf("expected scheme ssh, got %s", u.Scheme) + } +} + +func TestParseURL_HTTPSScheme(t *testing.T) { + u, err := transport.ParseURL("https://github.com/owner/repo.git") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if u.Scheme != "https" { + t.Fatalf("expected scheme https, got %s", u.Scheme) + } +} + +func TestParseURL_HTTPScheme(t *testing.T) { + u, err := transport.ParseURL("http://github.com/owner/repo.git") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if u.Scheme != "http" { + t.Fatalf("expected scheme http, got %s", u.Scheme) + } +} \ No newline at end of file From 4347b269f1f7f6830a08797978a712d7c19d8390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 29 Apr 2026 17:47:47 +0200 Subject: [PATCH 02/10] feat: add SSH support to e2e test utilities Add CreateSSHKey and SSHRepoURL to RepositoryProvider interface. Add WithSSHKey option for InitializeRepoWithFunction to push via SSH. Read SSH endpoint from gitea-endpoint ConfigMap during client init. --- test/utils/gitea.go | 51 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/test/utils/gitea.go b/test/utils/gitea.go index 797a908..6f4cc0e 100644 --- a/test/utils/gitea.go +++ b/test/utils/gitea.go @@ -46,14 +46,19 @@ type RepositoryProvider interface { // Authentication CreateAccessToken(username, password, tokenName string) (string, error) + + // SSH support + CreateSSHKey(username, password, title, publicKey string) error + SSHRepoURL(owner, repo string) (string, error) } // GiteaClient wraps the Gitea SDK client and provides helper methods type GiteaClient struct { - client *gitea.Client - baseURL string - adminUser string - adminPass string + client *gitea.Client + baseURL string + sshEndpoint string + adminUser string + adminPass string } // NewGiteaClient discovers Gitea endpoint from ConfigMap and creates client @@ -87,6 +92,11 @@ func NewGiteaClient() (*GiteaClient, error) { return nil, fmt.Errorf("gitea-endpoint configmap missing 'http' key") } + sshEndpoint, ok := cm.Data["ssh"] + if !ok { + return nil, fmt.Errorf("gitea-endpoint configmap missing 'ssh' key") + } + // Create Gitea SDK client giteaClient, err := gitea.NewClient(baseURL, gitea.SetBasicAuth(giteaAdminUser, giteaAdminPass)) if err != nil { @@ -94,10 +104,11 @@ func NewGiteaClient() (*GiteaClient, error) { } return &GiteaClient{ - client: giteaClient, - baseURL: baseURL, - adminUser: giteaAdminUser, - adminPass: giteaAdminPass, + client: giteaClient, + baseURL: baseURL, + sshEndpoint: sshEndpoint, + adminUser: giteaAdminUser, + adminPass: giteaAdminPass, }, nil } @@ -175,6 +186,30 @@ func (g *GiteaClient) CreateRandomRepo(owner string, private bool) (name, url st return name, url, cleanup, err } +// CreateSSHKey registers an SSH public key for a Gitea user +func (g *GiteaClient) CreateSSHKey(username, password, title, publicKey string) error { + userClient, err := gitea.NewClient(g.baseURL, gitea.SetBasicAuth(username, password)) + if err != nil { + return fmt.Errorf("failed to create user client: %w", err) + } + + _, _, err = userClient.CreatePublicKey(gitea.CreateKeyOption{ + Title: title, + Key: publicKey, + }) + if err != nil { + return fmt.Errorf("failed to create SSH key for %s: %w", username, err) + } + + return nil +} + +// SSHRepoURL returns the SSH URL for a repository. +// The SSH endpoint format from the ConfigMap is "host:port". +func (g *GiteaClient) SSHRepoURL(owner, repo string) (string, error) { + return fmt.Sprintf("ssh://git@%s/%s/%s.git", g.sshEndpoint, owner, repo), nil +} + // CreateAccessToken creates a personal access token for a user func (g *GiteaClient) CreateAccessToken(username, password, tokenName string) (string, error) { // Create a client authenticated as the user From 05eb2fa06ed0d97023ddea8cb3e54af91622f322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 29 Apr 2026 18:09:22 +0200 Subject: [PATCH 03/10] test: add SSH URL e2e tests for Function CR Add tests for public SSH repos, private SSH repos with key auth, and private SSH repos without auth (expected failure). Extend auth error matcher with SSH-specific error patterns. --- test/e2e/func_deploy_test.go | 210 +++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/test/e2e/func_deploy_test.go b/test/e2e/func_deploy_test.go index 8eafc14..325cd1b 100644 --- a/test/e2e/func_deploy_test.go +++ b/test/e2e/func_deploy_test.go @@ -106,6 +106,9 @@ func functionNotReadyWithAuthError(functionName, functionNamespace string) func( ContainSubstring("Authentication"), ContainSubstring("401"), ContainSubstring("Unauthorized"), + ContainSubstring("handshake failed"), + ContainSubstring("permission denied"), + ContainSubstring("ssh:"), )) return } @@ -528,4 +531,211 @@ var _ = Describe("Operator", func() { }) }) }) + Context("with an SSH repository URL", func() { + var sshRepoURL string + var repoDir string + var functionName, functionNamespace string + + BeforeEach(func() { + username, password, _, cleanup, err := repoProvider.CreateRandomUser() + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(cleanup) + + repoName, repoURL, cleanup, err := repoProvider.CreateRandomRepo(username, false) + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(cleanup) + + sshRepoURL, err = repoProvider.SSHRepoURL(username, repoName) + Expect(err).NotTo(HaveOccurred()) + + // Initialize repository with function code (via HTTP) + repoDir, err = utils.InitializeRepoWithFunction(repoURL, username, password, "go") + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(os.RemoveAll, repoDir) + + functionNamespace, err = utils.GetTestNamespace() + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(cleanupNamespaces, functionNamespace) + + // Deploy function using func CLI + out, err := utils.RunFuncDeploy(repoDir, utils.WithNamespace(functionNamespace)) + Expect(err).NotTo(HaveOccurred()) + _, _ = fmt.Fprint(GinkgoWriter, out) + + utils.DeferCleanupOnSuccess(func() { + _, _ = utils.RunFunc("delete", "--path", repoDir, "--namespace", functionNamespace) + }) + + // Commit func.yaml changes + err = utils.CommitAndPush(repoDir, "Update func.yaml after deploy", "func.yaml") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + logFailedTestDetails(functionName, functionNamespace) + + if functionName != "" { + cmd := exec.Command("kubectl", "delete", "function", functionName, "-n", functionNamespace, "--ignore-not-found") + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + } + }) + + It("should mark the function as ready with a public SSH repo", func() { + function := &functionsdevv1alpha1.Function{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "my-ssh-function-", + Namespace: functionNamespace, + }, + Spec: functionsdevv1alpha1.FunctionSpec{ + Repository: functionsdevv1alpha1.FunctionSpecRepository{ + URL: sshRepoURL, + }, + }, + } + + err := k8sClient.Create(ctx, function) + Expect(err).NotTo(HaveOccurred()) + + functionName = function.Name + + Eventually(functionBecomesReady(functionName, functionNamespace)).Should(Succeed()) + }) + }) + Context("with a private SSH repository", func() { + var sshRepoURL string + var repoDir string + var username, password string + var sshKeyPath string + var functionName, functionNamespace string + + BeforeEach(func() { + var cleanup func() + var err error + var repoName string + var repoURL string + + username, password, _, cleanup, err = repoProvider.CreateRandomUser() + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(cleanup) + + repoName, repoURL, cleanup, err = repoProvider.CreateRandomRepo(username, true) + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(cleanup) + + // Generate SSH keypair and register with Gitea + keyDir, err := os.MkdirTemp("", "ssh-e2e-*") + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(os.RemoveAll, keyDir) + + sshKeyPath = filepath.Join(keyDir, "id_ed25519") + cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", sshKeyPath, "-N", "", "-q") + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + + pubKeyBytes, err := os.ReadFile(sshKeyPath + ".pub") + Expect(err).NotTo(HaveOccurred()) + + err = repoProvider.CreateSSHKey(username, password, "e2e-key", string(pubKeyBytes)) + Expect(err).NotTo(HaveOccurred()) + + sshRepoURL, err = repoProvider.SSHRepoURL(username, repoName) + Expect(err).NotTo(HaveOccurred()) + + // Initialize repository with function code (via HTTP) + repoDir, err = utils.InitializeRepoWithFunction(repoURL, username, password, "go") + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(os.RemoveAll, repoDir) + + functionNamespace, err = utils.GetTestNamespace() + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(cleanupNamespaces, functionNamespace) + + // Deploy function using func CLI + out, err := utils.RunFuncDeploy(repoDir, utils.WithNamespace(functionNamespace)) + Expect(err).NotTo(HaveOccurred()) + _, _ = fmt.Fprint(GinkgoWriter, out) + + utils.DeferCleanupOnSuccess(func() { + _, _ = utils.RunFunc("delete", "--path", repoDir, "--namespace", functionNamespace) + }) + + // Commit func.yaml changes + err = utils.CommitAndPush(repoDir, "Update func.yaml after deploy", "func.yaml") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + logFailedTestDetails(functionName, functionNamespace) + + if functionName != "" { + cmd := exec.Command("kubectl", "delete", "function", functionName, "-n", functionNamespace, "--ignore-not-found") + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + } + }) + + It("should mark the function as ready when SSH key authSecretRef is provided", func() { + privateKeyBytes, err := os.ReadFile(sshKeyPath) + Expect(err).NotTo(HaveOccurred()) + + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "git-ssh-auth-", + Namespace: functionNamespace, + }, + Data: map[string][]byte{ + "sshPrivateKey": privateKeyBytes, + }, + } + err = k8sClient.Create(ctx, secret) + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(func() { + _ = k8sClient.Delete(ctx, secret) + }) + + function := &functionsdevv1alpha1.Function{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "my-ssh-private-function-", + Namespace: functionNamespace, + }, + Spec: functionsdevv1alpha1.FunctionSpec{ + Repository: functionsdevv1alpha1.FunctionSpecRepository{ + URL: sshRepoURL, + AuthSecretRef: &v1.LocalObjectReference{ + Name: secret.Name, + }, + }, + }, + } + + err = k8sClient.Create(ctx, function) + Expect(err).NotTo(HaveOccurred()) + + functionName = function.Name + + Eventually(functionBecomesReady(functionName, functionNamespace)).Should(Succeed()) + }) + + It("should fail with authentication error when authSecretRef is not provided", func() { + function := &functionsdevv1alpha1.Function{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "my-ssh-private-function-noauth-", + Namespace: functionNamespace, + }, + Spec: functionsdevv1alpha1.FunctionSpec{ + Repository: functionsdevv1alpha1.FunctionSpecRepository{ + URL: sshRepoURL, + }, + }, + } + + err := k8sClient.Create(ctx, function) + Expect(err).NotTo(HaveOccurred()) + + functionName = function.Name + + Eventually(functionNotReadyWithAuthError(functionName, functionNamespace), 2*time.Minute).Should(Succeed()) + }) + }) }) From 595cec636baa094d233746e31f7a248de623683a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 29 Apr 2026 18:10:51 +0200 Subject: [PATCH 04/10] docs: add SSH repository URL documentation Document SSH key authentication secret format with sshPrivateKey, sshPrivateKeyPassword, and known_hosts fields. Add Function CR examples for both private and public SSH repos. Update API reference table with supported URL formats. --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e255681..aab8409 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,66 @@ spec: name: git-credentials ``` +#### SSH Key Authentication + +For SSH-based repository access, create a secret with the SSH private key: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: git-ssh-credentials + namespace: default +data: + sshPrivateKey: +``` + +Optional fields: +- `sshPrivateKeyPassword`: Passphrase for encrypted private keys +- `known_hosts`: SSH known_hosts file content for host key verification. If omitted, host key checking is skipped. + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: git-ssh-credentials + namespace: default +data: + sshPrivateKey: + sshPrivateKeyPassword: + known_hosts: +``` + +Reference it in the Function with an SSH repository URL: + +```yaml +apiVersion: functions.dev/v1alpha1 +kind: Function +metadata: + name: my-function + namespace: default +spec: + repository: + url: git@github.com:your-org/your-function.git + authSecretRef: + name: git-ssh-credentials +``` + +For public repositories accessible over SSH, no secret is needed: + +```yaml +apiVersion: functions.dev/v1alpha1 +kind: Function +metadata: + name: my-function + namespace: default +spec: + repository: + url: git@github.com:your-org/your-function.git +``` + +Both SCP-style URLs (`git@host:path`) and standard SSH URLs (`ssh://git@host/path`) are supported. + ### Check Function Status The Function CRD has the short name `func`, so you can use `kubectl get func` instead of `kubectl get function`. @@ -300,7 +360,7 @@ make lint | Field | Type | Required | Description | |-----------------------------|---------|----------|--------------------------------------------------------------------------------------------------| -| `repository.url` | string | Yes | URL of the Git repository containing the function | +| `repository.url` | string | Yes | URL of the Git repository. Supports HTTPS, HTTP, SSH (`ssh://`), and SCP-style (`git@host:path`) | | `repository.branch` | string | No | Branch of the repository | | `repository.path` | string | No | Path to the function inside the repository. Defaults to "." | | `repository.authSecretRef` | object | No | Reference to the auth secret for private repository authentication | From e1f2e594d915965c90f04627725ea256530f7b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 29 Apr 2026 18:14:28 +0200 Subject: [PATCH 05/10] fix: address lint issues in git manager SSH support Check error return from tmpFile.Close() and extract repeated "ssh" string literal into constant. --- internal/git/manager.go | 2 +- internal/git/manager_test.go | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/git/manager.go b/internal/git/manager.go index 05e751f..042cc6d 100644 --- a/internal/git/manager.go +++ b/internal/git/manager.go @@ -128,7 +128,7 @@ func (m *managerImpl) getSSHClientOptions(authSecret map[string][]byte) []client if err == nil { defer os.Remove(tmpFile.Name()) if _, err := tmpFile.Write(knownHostsData); err == nil { - tmpFile.Close() + _ = tmpFile.Close() db, err := knownhosts.NewDB(tmpFile.Name()) if err == nil { auth.HostKeyCallback = db.HostKeyCallback() diff --git a/internal/git/manager_test.go b/internal/git/manager_test.go index 9f39f09..c9d7a58 100644 --- a/internal/git/manager_test.go +++ b/internal/git/manager_test.go @@ -6,6 +6,8 @@ import ( "github.com/go-git/go-git/v6/plumbing/transport" ) +const sshScheme = "ssh" + const testEd25519PrivateKey = `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACDAtq/Kt1/J1J/YivGDJIO57fFW1v68f1eq1N1Vr77BLAAAALB+/pd5fv6X @@ -43,7 +45,7 @@ func TestGetClientOptions_HTTPEmpty(t *testing.T) { func TestGetClientOptions_SSHNoSecret(t *testing.T) { m := &managerImpl{} - opts := m.getClientOptions("ssh", nil) + opts := m.getClientOptions(sshScheme, nil) if len(opts) != 1 { t.Fatalf("expected 1 option for insecure SSH, got %d", len(opts)) } @@ -51,7 +53,7 @@ func TestGetClientOptions_SSHNoSecret(t *testing.T) { func TestGetClientOptions_SSHEmptySecret(t *testing.T) { m := &managerImpl{} - opts := m.getClientOptions("ssh", map[string][]byte{}) + opts := m.getClientOptions(sshScheme, map[string][]byte{}) if len(opts) != 1 { t.Fatalf("expected 1 option for insecure SSH, got %d", len(opts)) } @@ -60,7 +62,7 @@ func TestGetClientOptions_SSHEmptySecret(t *testing.T) { func TestGetClientOptions_SSHWithPrivateKey(t *testing.T) { m := &managerImpl{} secret := map[string][]byte{"sshPrivateKey": []byte(testEd25519PrivateKey)} - opts := m.getClientOptions("ssh", secret) + opts := m.getClientOptions(sshScheme, secret) if len(opts) != 1 { t.Fatalf("expected 1 option, got %d", len(opts)) } @@ -69,7 +71,7 @@ func TestGetClientOptions_SSHWithPrivateKey(t *testing.T) { func TestGetClientOptions_SSHWithInvalidKey(t *testing.T) { m := &managerImpl{} secret := map[string][]byte{"sshPrivateKey": []byte("not-a-valid-key")} - opts := m.getClientOptions("ssh", secret) + opts := m.getClientOptions(sshScheme, secret) if opts != nil { t.Fatalf("expected nil options for invalid key, got %v", opts) } @@ -80,7 +82,7 @@ func TestParseURL_SCPStyle(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if u.Scheme != "ssh" { + if u.Scheme != sshScheme { t.Fatalf("expected scheme ssh, got %s", u.Scheme) } } @@ -90,7 +92,7 @@ func TestParseURL_SSHScheme(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if u.Scheme != "ssh" { + if u.Scheme != sshScheme { t.Fatalf("expected scheme ssh, got %s", u.Scheme) } } @@ -113,4 +115,4 @@ func TestParseURL_HTTPScheme(t *testing.T) { if u.Scheme != "http" { t.Fatalf("expected scheme http, got %s", u.Scheme) } -} \ No newline at end of file +} From 56a31caea813a3f74dd571c0248babdda55b5334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 29 Apr 2026 18:22:18 +0200 Subject: [PATCH 06/10] fix: use ssh.Password instead of ssh.PublicKeys for no-key SSH ssh.PublicKeys with a nil Signer panics when go-git calls ClientConfig(). Use ssh.Password with empty password instead, which creates a valid SSH config for public repo access. --- internal/git/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/git/manager.go b/internal/git/manager.go index 042cc6d..0d96a8f 100644 --- a/internal/git/manager.go +++ b/internal/git/manager.go @@ -107,7 +107,7 @@ func (m *managerImpl) getSSHClientOptions(authSecret map[string][]byte) []client privateKey, hasKey := authSecret["sshPrivateKey"] if !hasKey { return []client.Option{ - client.WithSSHAuth(&ssh.PublicKeys{ + client.WithSSHAuth(&ssh.Password{ User: "git", HostKeyCallbackHelper: ssh.HostKeyCallbackHelper{ HostKeyCallback: gossh.InsecureIgnoreHostKey(), From 2154a40e32dce90f58268de3eb0766783f3661c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 29 Apr 2026 18:30:58 +0200 Subject: [PATCH 07/10] fix: build SSH ClientConfig directly to set HostKeyAlgorithms go-git's SSH transport falls back to loading known_hosts when HostKeyAlgorithms is empty, even if HostKeyCallback is set. Build the gossh.ClientConfig directly instead of using ssh.Password or ssh.PublicKeys, setting both HostKeyCallback and HostKeyAlgorithms to prevent the known_hosts fallback in the operator container. --- internal/git/manager.go | 54 ++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/internal/git/manager.go b/internal/git/manager.go index 0d96a8f..3a4055b 100644 --- a/internal/git/manager.go +++ b/internal/git/manager.go @@ -11,7 +11,6 @@ import ( "github.com/go-git/go-git/v6/plumbing/client" "github.com/go-git/go-git/v6/plumbing/transport" "github.com/go-git/go-git/v6/plumbing/transport/http" - "github.com/go-git/go-git/v6/plumbing/transport/ssh" "github.com/go-git/go-git/v6/plumbing/transport/ssh/knownhosts" "github.com/prometheus/client_golang/prometheus" gossh "golang.org/x/crypto/ssh" @@ -103,26 +102,47 @@ func (m *managerImpl) getHTTPClientOptions(authSecret map[string][]byte) []clien return nil } +// sshAuthFunc is a function type that implements client.SSHAuth. +type sshAuthFunc func(context.Context, *transport.Request) (*gossh.ClientConfig, error) + +func (f sshAuthFunc) ClientConfig(ctx context.Context, req *transport.Request) (*gossh.ClientConfig, error) { + return f(ctx, req) +} + +var defaultHostKeyAlgorithms = []string{ + gossh.KeyAlgoED25519, + gossh.KeyAlgoECDSA256, gossh.KeyAlgoECDSA384, gossh.KeyAlgoECDSA521, + gossh.KeyAlgoRSASHA512, gossh.KeyAlgoRSASHA256, gossh.KeyAlgoRSA, +} + func (m *managerImpl) getSSHClientOptions(authSecret map[string][]byte) []client.Option { privateKey, hasKey := authSecret["sshPrivateKey"] if !hasKey { return []client.Option{ - client.WithSSHAuth(&ssh.Password{ - User: "git", - HostKeyCallbackHelper: ssh.HostKeyCallbackHelper{ - HostKeyCallback: gossh.InsecureIgnoreHostKey(), - }, - }), + client.WithSSHAuth(sshAuthFunc(func(_ context.Context, _ *transport.Request) (*gossh.ClientConfig, error) { + return &gossh.ClientConfig{ + User: "git", + HostKeyCallback: gossh.InsecureIgnoreHostKey(), + HostKeyAlgorithms: defaultHostKeyAlgorithms, + }, nil + })), } } password := string(authSecret["sshPrivateKeyPassword"]) - - auth, err := ssh.NewPublicKeys("git", privateKey, password) + signer, err := gossh.ParsePrivateKey(privateKey) if err != nil { - return nil + if _, ok := err.(*gossh.PassphraseMissingError); ok { + signer, err = gossh.ParsePrivateKeyWithPassphrase(privateKey, []byte(password)) + } + if err != nil { + return nil + } } + hostKeyCallback := gossh.InsecureIgnoreHostKey() + hostKeyAlgorithms := defaultHostKeyAlgorithms + if knownHostsData, ok := authSecret["known_hosts"]; ok { tmpFile, err := os.CreateTemp("", "known_hosts-*") if err == nil { @@ -131,15 +151,21 @@ func (m *managerImpl) getSSHClientOptions(authSecret map[string][]byte) []client _ = tmpFile.Close() db, err := knownhosts.NewDB(tmpFile.Name()) if err == nil { - auth.HostKeyCallback = db.HostKeyCallback() + hostKeyCallback = db.HostKeyCallback() + hostKeyAlgorithms = nil } } } - } else { - auth.HostKeyCallback = gossh.InsecureIgnoreHostKey() } return []client.Option{ - client.WithSSHAuth(auth), + client.WithSSHAuth(sshAuthFunc(func(_ context.Context, _ *transport.Request) (*gossh.ClientConfig, error) { + return &gossh.ClientConfig{ + User: "git", + Auth: []gossh.AuthMethod{gossh.PublicKeys(signer)}, + HostKeyCallback: hostKeyCallback, + HostKeyAlgorithms: hostKeyAlgorithms, + }, nil + })), } } From 1801c2e1e4e33b3bfda63d335b0bda75d160e35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Thu, 30 Apr 2026 08:17:51 +0200 Subject: [PATCH 08/10] fix: require SSH key auth in public SSH repo e2e test Gitea requires SSH key authentication for all SSH connections, even to public repos. The public SSH test now generates a keypair and provides it via authSecretRef. Also extracts a helper to deduplicate the SSH Function creation logic across test contexts. --- test/e2e/func_deploy_test.go | 126 +++++++++++++++++++---------------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/test/e2e/func_deploy_test.go b/test/e2e/func_deploy_test.go index 325cd1b..1e84cec 100644 --- a/test/e2e/func_deploy_test.go +++ b/test/e2e/func_deploy_test.go @@ -117,6 +117,52 @@ func functionNotReadyWithAuthError(functionName, functionNamespace string) func( } } +// createSSHFunctionAndExpectReady creates a K8s Secret with the SSH private key, creates a Function +// CR pointing at the SSH repo URL with authSecretRef, and waits for it to become Ready. +// It returns the Function name so callers can store it for cleanup. +func createSSHFunctionAndExpectReady( + sshKeyPath, sshRepoURL, functionNamespace, namePrefix string, +) string { + privateKeyBytes, err := os.ReadFile(sshKeyPath) + Expect(err).NotTo(HaveOccurred()) + + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "git-ssh-auth-", + Namespace: functionNamespace, + }, + Data: map[string][]byte{ + "sshPrivateKey": privateKeyBytes, + }, + } + err = k8sClient.Create(ctx, secret) + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(func() { + _ = k8sClient.Delete(ctx, secret) + }) + + function := &functionsdevv1alpha1.Function{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: namePrefix, + Namespace: functionNamespace, + }, + Spec: functionsdevv1alpha1.FunctionSpec{ + Repository: functionsdevv1alpha1.FunctionSpecRepository{ + URL: sshRepoURL, + AuthSecretRef: &v1.LocalObjectReference{ + Name: secret.Name, + }, + }, + }, + } + + err = k8sClient.Create(ctx, function) + Expect(err).NotTo(HaveOccurred()) + + Eventually(functionBecomesReady(function.Name, functionNamespace)).Should(Succeed()) + return function.Name +} + // functionNotDeployed check if the function is not ready as the function was not deployed yet func functionNotDeployed(functionName, functionNamespace string) func(g Gomega) { return expectFunctionConditionFalseWithReason( @@ -534,6 +580,7 @@ var _ = Describe("Operator", func() { Context("with an SSH repository URL", func() { var sshRepoURL string var repoDir string + var sshKeyPath string var functionName, functionNamespace string BeforeEach(func() { @@ -545,6 +592,22 @@ var _ = Describe("Operator", func() { Expect(err).NotTo(HaveOccurred()) utils.DeferCleanupOnSuccess(cleanup) + // Generate SSH keypair and register with Gitea + keyDir, err := os.MkdirTemp("", "ssh-e2e-*") + Expect(err).NotTo(HaveOccurred()) + utils.DeferCleanupOnSuccess(os.RemoveAll, keyDir) + + sshKeyPath = filepath.Join(keyDir, "id_ed25519") + cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", sshKeyPath, "-N", "", "-q") + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + + pubKeyBytes, err := os.ReadFile(sshKeyPath + ".pub") + Expect(err).NotTo(HaveOccurred()) + + err = repoProvider.CreateSSHKey(username, password, "e2e-key", string(pubKeyBytes)) + Expect(err).NotTo(HaveOccurred()) + sshRepoURL, err = repoProvider.SSHRepoURL(username, repoName) Expect(err).NotTo(HaveOccurred()) @@ -581,25 +644,9 @@ var _ = Describe("Operator", func() { } }) - It("should mark the function as ready with a public SSH repo", func() { - function := &functionsdevv1alpha1.Function{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: "my-ssh-function-", - Namespace: functionNamespace, - }, - Spec: functionsdevv1alpha1.FunctionSpec{ - Repository: functionsdevv1alpha1.FunctionSpecRepository{ - URL: sshRepoURL, - }, - }, - } - - err := k8sClient.Create(ctx, function) - Expect(err).NotTo(HaveOccurred()) - - functionName = function.Name - - Eventually(functionBecomesReady(functionName, functionNamespace)).Should(Succeed()) + It("should mark the function as ready with SSH key auth", func() { + functionName = createSSHFunctionAndExpectReady( + sshKeyPath, sshRepoURL, functionNamespace, "my-ssh-function-") }) }) Context("with a private SSH repository", func() { @@ -676,45 +723,8 @@ var _ = Describe("Operator", func() { }) It("should mark the function as ready when SSH key authSecretRef is provided", func() { - privateKeyBytes, err := os.ReadFile(sshKeyPath) - Expect(err).NotTo(HaveOccurred()) - - secret := &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: "git-ssh-auth-", - Namespace: functionNamespace, - }, - Data: map[string][]byte{ - "sshPrivateKey": privateKeyBytes, - }, - } - err = k8sClient.Create(ctx, secret) - Expect(err).NotTo(HaveOccurred()) - utils.DeferCleanupOnSuccess(func() { - _ = k8sClient.Delete(ctx, secret) - }) - - function := &functionsdevv1alpha1.Function{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: "my-ssh-private-function-", - Namespace: functionNamespace, - }, - Spec: functionsdevv1alpha1.FunctionSpec{ - Repository: functionsdevv1alpha1.FunctionSpecRepository{ - URL: sshRepoURL, - AuthSecretRef: &v1.LocalObjectReference{ - Name: secret.Name, - }, - }, - }, - } - - err = k8sClient.Create(ctx, function) - Expect(err).NotTo(HaveOccurred()) - - functionName = function.Name - - Eventually(functionBecomesReady(functionName, functionNamespace)).Should(Succeed()) + functionName = createSSHFunctionAndExpectReady( + sshKeyPath, sshRepoURL, functionNamespace, "my-ssh-private-function-") }) It("should fail with authentication error when authSecretRef is not provided", func() { From 1f33f76aaa78907b8dadb6d2394c7876f0dc8f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Thu, 30 Apr 2026 08:42:14 +0200 Subject: [PATCH 09/10] docs: explain why SSH auth uses custom ClientConfig builder go-git's SSH transport tries to load known_hosts for HostKeyAlgorithms even when HostKeyCallback is already set, which fails in containers. Document why we build gossh.ClientConfig directly instead of using go-git's built-in SSH auth types. --- internal/git/manager.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/git/manager.go b/internal/git/manager.go index 3a4055b..02670e0 100644 --- a/internal/git/manager.go +++ b/internal/git/manager.go @@ -102,13 +102,21 @@ func (m *managerImpl) getHTTPClientOptions(authSecret map[string][]byte) []clien return nil } -// sshAuthFunc is a function type that implements client.SSHAuth. +// sshAuthFunc implements client.SSHAuth by building gossh.ClientConfig directly. +// We cannot use go-git's built-in SSH auth types (ssh.PublicKeys, ssh.Password) +// because go-git's SSH transport tries to load known_hosts files to populate +// HostKeyAlgorithms even when HostKeyCallback is already set (ssh.go:86-92). +// In containers without ~/.ssh/known_hosts, this causes a hard error. Building +// the ClientConfig ourselves with both HostKeyCallback and HostKeyAlgorithms +// set avoids this code path entirely. type sshAuthFunc func(context.Context, *transport.Request) (*gossh.ClientConfig, error) func (f sshAuthFunc) ClientConfig(ctx context.Context, req *transport.Request) (*gossh.ClientConfig, error) { return f(ctx, req) } +// defaultHostKeyAlgorithms must be non-empty to prevent go-git's SSH transport +// from falling back to loading known_hosts files for algorithm discovery. var defaultHostKeyAlgorithms = []string{ gossh.KeyAlgoED25519, gossh.KeyAlgoECDSA256, gossh.KeyAlgoECDSA384, gossh.KeyAlgoECDSA521, From 1839f1c6ae17fb9d1dd808f901f36283024ec408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Thu, 30 Apr 2026 09:08:38 +0200 Subject: [PATCH 10/10] refactor: use go-git built-in SSH types instead of custom ClientConfig builder Create an empty ~/.ssh/known_hosts in NewManager() so go-git's SSH transport can resolve host key algorithms without error. This lets us use gitssh.Password and gitssh.NewPublicKeys directly, removing the custom sshAuthFunc type and defaultHostKeyAlgorithms list. --- internal/git/manager.go | 82 ++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 50 deletions(-) diff --git a/internal/git/manager.go b/internal/git/manager.go index 02670e0..d2576f0 100644 --- a/internal/git/manager.go +++ b/internal/git/manager.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path/filepath" "github.com/functions-dev/func-operator/internal/monitoring" "github.com/go-git/go-git/v6" @@ -11,7 +12,7 @@ import ( "github.com/go-git/go-git/v6/plumbing/client" "github.com/go-git/go-git/v6/plumbing/transport" "github.com/go-git/go-git/v6/plumbing/transport/http" - "github.com/go-git/go-git/v6/plumbing/transport/ssh/knownhosts" + gitssh "github.com/go-git/go-git/v6/plumbing/transport/ssh" "github.com/prometheus/client_golang/prometheus" gossh "golang.org/x/crypto/ssh" ) @@ -28,6 +29,12 @@ func NewManager() (Manager, error) { if err := os.MkdirAll(cloneBaseDir, 0755); err != nil { return nil, fmt.Errorf("failed to create git clone base directory: %w", err) } + // go-git's SSH transport requires a known_hosts file for host key algorithm + // discovery, even when HostKeyCallback is already set. Without this file, + // SSH connections fail in containers that lack ~/.ssh/known_hosts. + if err := ensureKnownHostsExists(); err != nil { + return nil, fmt.Errorf("failed to ensure known_hosts exists: %w", err) + } return &managerImpl{}, nil } @@ -102,54 +109,39 @@ func (m *managerImpl) getHTTPClientOptions(authSecret map[string][]byte) []clien return nil } -// sshAuthFunc implements client.SSHAuth by building gossh.ClientConfig directly. -// We cannot use go-git's built-in SSH auth types (ssh.PublicKeys, ssh.Password) -// because go-git's SSH transport tries to load known_hosts files to populate -// HostKeyAlgorithms even when HostKeyCallback is already set (ssh.go:86-92). -// In containers without ~/.ssh/known_hosts, this causes a hard error. Building -// the ClientConfig ourselves with both HostKeyCallback and HostKeyAlgorithms -// set avoids this code path entirely. -type sshAuthFunc func(context.Context, *transport.Request) (*gossh.ClientConfig, error) - -func (f sshAuthFunc) ClientConfig(ctx context.Context, req *transport.Request) (*gossh.ClientConfig, error) { - return f(ctx, req) -} - -// defaultHostKeyAlgorithms must be non-empty to prevent go-git's SSH transport -// from falling back to loading known_hosts files for algorithm discovery. -var defaultHostKeyAlgorithms = []string{ - gossh.KeyAlgoED25519, - gossh.KeyAlgoECDSA256, gossh.KeyAlgoECDSA384, gossh.KeyAlgoECDSA521, - gossh.KeyAlgoRSASHA512, gossh.KeyAlgoRSASHA256, gossh.KeyAlgoRSA, +func ensureKnownHostsExists() error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + sshDir := filepath.Join(home, ".ssh") + if err := os.MkdirAll(sshDir, 0700); err != nil { + return err + } + knownHostsPath := filepath.Join(sshDir, "known_hosts") + if _, err := os.Stat(knownHostsPath); os.IsNotExist(err) { + return os.WriteFile(knownHostsPath, nil, 0644) + } + return nil } func (m *managerImpl) getSSHClientOptions(authSecret map[string][]byte) []client.Option { privateKey, hasKey := authSecret["sshPrivateKey"] if !hasKey { return []client.Option{ - client.WithSSHAuth(sshAuthFunc(func(_ context.Context, _ *transport.Request) (*gossh.ClientConfig, error) { - return &gossh.ClientConfig{ - User: "git", - HostKeyCallback: gossh.InsecureIgnoreHostKey(), - HostKeyAlgorithms: defaultHostKeyAlgorithms, - }, nil - })), + client.WithSSHAuth(&gitssh.Password{ + User: "git", + HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{HostKeyCallback: gossh.InsecureIgnoreHostKey()}, + }), } } password := string(authSecret["sshPrivateKeyPassword"]) - signer, err := gossh.ParsePrivateKey(privateKey) + auth, err := gitssh.NewPublicKeys("git", privateKey, password) if err != nil { - if _, ok := err.(*gossh.PassphraseMissingError); ok { - signer, err = gossh.ParsePrivateKeyWithPassphrase(privateKey, []byte(password)) - } - if err != nil { - return nil - } + return nil } - - hostKeyCallback := gossh.InsecureIgnoreHostKey() - hostKeyAlgorithms := defaultHostKeyAlgorithms + auth.HostKeyCallback = gossh.InsecureIgnoreHostKey() if knownHostsData, ok := authSecret["known_hosts"]; ok { tmpFile, err := os.CreateTemp("", "known_hosts-*") @@ -157,23 +149,13 @@ func (m *managerImpl) getSSHClientOptions(authSecret map[string][]byte) []client defer os.Remove(tmpFile.Name()) if _, err := tmpFile.Write(knownHostsData); err == nil { _ = tmpFile.Close() - db, err := knownhosts.NewDB(tmpFile.Name()) + cb, err := gitssh.NewKnownHostsCallback(tmpFile.Name()) if err == nil { - hostKeyCallback = db.HostKeyCallback() - hostKeyAlgorithms = nil + auth.HostKeyCallback = cb } } } } - return []client.Option{ - client.WithSSHAuth(sshAuthFunc(func(_ context.Context, _ *transport.Request) (*gossh.ClientConfig, error) { - return &gossh.ClientConfig{ - User: "git", - Auth: []gossh.AuthMethod{gossh.PublicKeys(signer)}, - HostKeyCallback: hostKeyCallback, - HostKeyAlgorithms: hostKeyAlgorithms, - }, nil - })), - } + return []client.Option{client.WithSSHAuth(auth)} }