diff --git a/go.mod b/go.mod
index 77012f1103bc7..3c35132910093 100644
--- a/go.mod
+++ b/go.mod
@@ -101,6 +101,7 @@ require (
github.com/fxamacker/cbor/v2 v2.7.0
github.com/ghodss/yaml v1.0.0
github.com/gizak/termui/v3 v3.1.0
+ github.com/go-git/go-git/v5 v5.13.1
github.com/go-jose/go-jose/v3 v3.0.3
github.com/go-ldap/ldap/v3 v3.4.10
github.com/go-logr/logr v1.4.2
@@ -308,7 +309,7 @@ require (
github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf // indirect
github.com/crewjam/httperr v0.2.0 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46 // indirect
- github.com/cyphar/filepath-securejoin v0.3.4 // indirect
+ github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/danieljoos/wincred v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/daviddengcn/go-colortext v1.0.0 // indirect
@@ -341,6 +342,8 @@ require (
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/go-git/go-billy/v5 v5.6.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -402,6 +405,7 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
@@ -469,6 +473,7 @@ require (
github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect
github.com/pingcap/log v1.1.1-0.20230317032135-a0d097d16e22 // indirect
github.com/pingcap/tidb/pkg/parser v0.0.0-20240930120915-74034d4ac243 // indirect
+ github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/xattr v0.4.10 // indirect
@@ -548,6 +553,7 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
k8s.io/component-helpers v0.31.3 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/metrics v0.31.3 // indirect
diff --git a/go.sum b/go.sum
index c270f81730944..5665c4f7280c7 100644
--- a/go.sum
+++ b/go.sum
@@ -778,8 +778,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
-github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
-github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
+github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
+github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E=
@@ -1104,8 +1104,8 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46 h1:2Dx4IHfC1yHWI12AxQDJM1QbRCDfk6M+blLzlZCXdrc=
github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
-github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
-github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
+github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
+github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -1247,6 +1247,14 @@ github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
+github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
+github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
+github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -1689,6 +1697,8 @@ github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
@@ -1973,6 +1983,8 @@ github.com/pingcap/log v1.1.1-0.20230317032135-a0d097d16e22 h1:2SOzvGvE8beiC1Y4g
github.com/pingcap/log v1.1.1-0.20230317032135-a0d097d16e22/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4=
github.com/pingcap/tidb/pkg/parser v0.0.0-20240930120915-74034d4ac243 h1:B3pF5adXRpuEDfSKY/bV2Lw+pPKtWH4FOaAX3Jx3X54=
github.com/pingcap/tidb/pkg/parser v0.0.0-20240930120915-74034d4ac243/go.mod h1:dXcO3Ts6jUVE1VwBZp3wbVdGO4pi9MXY6IvL4L1z62g=
+github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
+github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -2081,8 +2093,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
-github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
-github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500/go.mod h1:+njLrG5wSeoG4Ds61rFgEzKvenR2UHbjMoDHsczxly0=
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df h1:S77Pf5fIGMa7oSwp8SQPp7Hb4ZiI38K3RNBKD2LLeEM=
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc=
@@ -3148,6 +3160,8 @@ gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/integrations/event-handler/go.mod b/integrations/event-handler/go.mod
index 922ce6e72ae3c..19d919b359e39 100644
--- a/integrations/event-handler/go.mod
+++ b/integrations/event-handler/go.mod
@@ -115,7 +115,7 @@ require (
github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf // indirect
github.com/crewjam/httperr v0.2.0 // indirect
github.com/crewjam/saml v0.4.14 // indirect
- github.com/cyphar/filepath-securejoin v0.3.4 // indirect
+ github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/di-wu/parser v0.3.0 // indirect
github.com/di-wu/xsd-datetime v1.0.0 // indirect
diff --git a/integrations/event-handler/go.sum b/integrations/event-handler/go.sum
index e88669e71da35..1f0435df0d184 100644
--- a/integrations/event-handler/go.sum
+++ b/integrations/event-handler/go.sum
@@ -883,8 +883,8 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
-github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
-github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
+github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
+github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
diff --git a/integrations/terraform/go.mod b/integrations/terraform/go.mod
index 50246feeb9ed6..d3240ffff8135 100644
--- a/integrations/terraform/go.mod
+++ b/integrations/terraform/go.mod
@@ -131,7 +131,7 @@ require (
github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf // indirect
github.com/crewjam/httperr v0.2.0 // indirect
github.com/crewjam/saml v0.4.14 // indirect
- github.com/cyphar/filepath-securejoin v0.3.4 // indirect
+ github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/di-wu/parser v0.3.0 // indirect
github.com/di-wu/xsd-datetime v1.0.0 // indirect
diff --git a/integrations/terraform/go.sum b/integrations/terraform/go.sum
index 1a6cf422dd62e..106e4e41c759b 100644
--- a/integrations/terraform/go.sum
+++ b/integrations/terraform/go.sum
@@ -971,8 +971,8 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
-github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
-github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
+github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
+github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -1088,12 +1088,12 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
-github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
-github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
+github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
+github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
-github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
-github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
+github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
+github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
diff --git a/tool/tsh/common/git.go b/tool/tsh/common/git.go
index 3f43578fb4132..990ddb8f22fc7 100644
--- a/tool/tsh/common/git.go
+++ b/tool/tsh/common/git.go
@@ -19,24 +19,128 @@
package common
import (
+ "bytes"
+ "io"
+ "os/exec"
+ "strings"
+
"github.com/alecthomas/kingpin/v2"
+ "github.com/go-git/go-git/v5/plumbing/transport"
+ "github.com/gravitational/trace"
+
+ "github.com/gravitational/teleport/api/types"
)
type gitCommands struct {
- list *gitListCommand
- login *gitLoginCommand
+ list *gitListCommand
+ login *gitLoginCommand
+ ssh *gitSSHCommand
+ config *gitConfigCommand
+ clone *gitCloneCommand
}
func newGitCommands(app *kingpin.Application) gitCommands {
git := app.Command("git", "Git server commands.")
cmds := gitCommands{
- login: newGitLoginCommand(git),
- list: newGitListCommand(git),
+ login: newGitLoginCommand(git),
+ list: newGitListCommand(git),
+ ssh: newGitSSHCommand(git),
+ config: newGitConfigCommand(git),
+ clone: newGitCloneCommand(git),
}
// TODO(greedy52) hide the commands until all basic features are implemented.
git.Hidden()
cmds.login.Hidden()
cmds.list.Hidden()
+ cmds.config.Hidden()
+ cmds.clone.Hidden()
return cmds
}
+
+type gitSSHURL transport.Endpoint
+
+func (g gitSSHURL) check() error {
+ switch {
+ case g.isGitHub():
+ if err := types.ValidateGitHubOrganizationName(g.owner()); err != nil {
+ return trace.Wrap(err)
+ }
+ }
+ return nil
+}
+
+func (g gitSSHURL) isGitHub() bool {
+ return g.Host == "github.com"
+}
+
+// owner returns the first part of the path. If the path does not have an owner,
+// an empty string is returned.
+//
+// For GitHub, owner is either the user or the organization that owns the repo.
+//
+// For example, if the SSH url is git@github.com:gravitational/teleport.git, the
+// owner would be "gravitational".
+func (g gitSSHURL) owner() string {
+ // g.Path may have a preceding "/" from url.Parse.
+ owner, _, ok := strings.Cut(strings.TrimPrefix(g.Path, "/"), "/")
+ if !ok {
+ return ""
+ }
+ return owner
+}
+
+// parseGitSSHURL parse a Git SSH URL.
+//
+// Git URL Spec:
+// - spec: https://git-scm.com/docs/git-clone#_git_urls
+// - example: ssh://example.org/path/to/repo.git
+//
+// GitHub (SCP-like) URL:
+// - spec: https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories
+// - example: git@github.com:gravitational/teleport.git
+func parseGitSSHURL(originalURL string) (*gitSSHURL, error) {
+ endpoint, err := transport.NewEndpoint(originalURL)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if endpoint.Protocol != "ssh" {
+ return nil, trace.BadParameter("unsupported git ssh URL %s", originalURL)
+ }
+ s := gitSSHURL(*endpoint)
+ if err := s.check(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &s, nil
+}
+
+func execGitAndCaptureStdout(cf *CLIConf, args ...string) (string, error) {
+ var bufStd bytes.Buffer
+ if err := execGitWithStdoutAndStderr(cf, &bufStd, cf.Stderr(), args...); err != nil {
+ return "", trace.Wrap(err)
+ }
+ return strings.TrimSpace(bufStd.String()), nil
+}
+
+func execGit(cf *CLIConf, args ...string) error {
+ return trace.Wrap(execGitWithStdoutAndStderr(cf, cf.Stdout(), cf.Stderr(), args...))
+}
+
+func execGitWithStdoutAndStderr(cf *CLIConf, stdout, stderr io.Writer, args ...string) error {
+ const gitExecutable = "git"
+ gitPath, err := cf.LookPath(gitExecutable)
+ if err != nil {
+ return trace.NotFound(`could not locate the executable %q. The following error occurred:
+%s
+
+tsh requires that the %q executable to be installed.
+You can install it by following the instructions at https://git-scm.com/book/en/v2/Getting-Started-Installing-Git`,
+ gitExecutable, err.Error(), gitExecutable)
+ }
+ logger.DebugContext(cf.Context, "Executing git command", "path", gitPath, "args", args)
+ cmd := exec.CommandContext(cf.Context, gitPath, args...)
+ cmd.Stdin = cf.Stdin()
+ cmd.Stdout = stdout
+ cmd.Stderr = stderr
+ return trace.Wrap(cf.RunCommand(cmd))
+}
diff --git a/tool/tsh/common/git_clone.go b/tool/tsh/common/git_clone.go
new file mode 100644
index 0000000000000..93d00d4134434
--- /dev/null
+++ b/tool/tsh/common/git_clone.go
@@ -0,0 +1,73 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package common
+
+import (
+ "fmt"
+
+ "github.com/alecthomas/kingpin/v2"
+ "github.com/gravitational/trace"
+)
+
+// gitCloneCommand implements `tsh git clone`.
+//
+// This command internally executes `git clone` while setting `core.sshcommand`.
+// You can generally assume the user has `git` binary installed (otherwise there
+// is no point using the `git` proxy feature).
+//
+// TODO(greedy52) investigate using `go-git` library instead of calling `git
+// clone`.
+type gitCloneCommand struct {
+ *kingpin.CmdClause
+
+ repository string
+ directory string
+}
+
+func newGitCloneCommand(parent *kingpin.CmdClause) *gitCloneCommand {
+ cmd := &gitCloneCommand{
+ CmdClause: parent.Command("clone", "Clone a Git repository."),
+ }
+
+ cmd.Arg("repository", "Git URL of the repository to clone.").Required().StringVar(&cmd.repository)
+ cmd.Arg("directory", "The name of a new directory to clone into.").StringVar(&cmd.directory)
+ // TODO(greedy52) support passing extra args to git like --branch/--depth.
+ return cmd
+}
+
+func (c *gitCloneCommand) run(cf *CLIConf) error {
+ u, err := parseGitSSHURL(c.repository)
+ if err != nil {
+ return trace.Wrap(err)
+ }
+ if !u.isGitHub() {
+ return trace.BadParameter("%s is not a GitHub repository", c.repository)
+ }
+
+ sshCommand := makeGitCoreSSHCommand(cf.executablePath, u.owner())
+ args := []string{
+ "clone",
+ "--config", fmt.Sprintf("%s=%s", gitCoreSSHCommand, sshCommand),
+ c.repository,
+ }
+ if c.directory != "" {
+ args = append(args, c.directory)
+ }
+ return trace.Wrap(execGit(cf, args...))
+}
diff --git a/tool/tsh/common/git_clone_test.go b/tool/tsh/common/git_clone_test.go
new file mode 100644
index 0000000000000..4e27e3ac3286f
--- /dev/null
+++ b/tool/tsh/common/git_clone_test.go
@@ -0,0 +1,115 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package common
+
+import (
+ "context"
+ "os/exec"
+ "slices"
+ "testing"
+
+ "github.com/gravitational/trace"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGitCloneCommand(t *testing.T) {
+ tests := []struct {
+ name string
+ cmd *gitCloneCommand
+ verifyCommand func(*exec.Cmd) error
+ checkError require.ErrorAssertionFunc
+ }{
+ {
+ name: "success",
+ cmd: &gitCloneCommand{
+ repository: "git@github.com:gravitational/teleport.git",
+ },
+ verifyCommand: func(cmd *exec.Cmd) error {
+ expect := []string{
+ "git", "clone",
+ "--config", "core.sshcommand=\"tsh\" git ssh --github-org gravitational",
+ "git@github.com:gravitational/teleport.git",
+ }
+ if !slices.Equal(expect, cmd.Args) {
+ return trace.CompareFailed("expect %v but got %v", expect, cmd.Args)
+ }
+ return nil
+ },
+ checkError: require.NoError,
+ },
+ {
+ name: "success with target dir",
+ cmd: &gitCloneCommand{
+ repository: "git@github.com:gravitational/teleport.git",
+ directory: "target_dir",
+ },
+ verifyCommand: func(cmd *exec.Cmd) error {
+ expect := []string{
+ "git", "clone",
+ "--config", "core.sshcommand=\"tsh\" git ssh --github-org gravitational",
+ "git@github.com:gravitational/teleport.git",
+ "target_dir",
+ }
+ if !slices.Equal(expect, cmd.Args) {
+ return trace.CompareFailed("expect %v but got %v", expect, cmd.Args)
+ }
+ return nil
+ },
+ checkError: require.NoError,
+ },
+ {
+ name: "invalid URL",
+ cmd: &gitCloneCommand{
+ repository: "not-a-git-ssh-url",
+ },
+ checkError: require.Error,
+ },
+ {
+ name: "unsupported Git service",
+ cmd: &gitCloneCommand{
+ repository: "git@gitlab.com:group/project.git",
+ },
+ checkError: require.Error,
+ },
+ {
+ name: "git fails",
+ cmd: &gitCloneCommand{
+ repository: "git@github.com:gravitational/teleport.git",
+ },
+ verifyCommand: func(cmd *exec.Cmd) error {
+ return trace.BadParameter("some git error")
+ },
+ checkError: func(t require.TestingT, err error, i ...interface{}) {
+ require.ErrorIs(t, err, trace.BadParameter("some git error"))
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cf := &CLIConf{
+ Context: context.Background(),
+ executablePath: "tsh",
+ cmdRunner: tt.verifyCommand,
+ lookPathOverride: "git",
+ }
+ tt.checkError(t, tt.cmd.run(cf))
+ })
+ }
+}
diff --git a/tool/tsh/common/git_config.go b/tool/tsh/common/git_config.go
new file mode 100644
index 0000000000000..89771735b30b3
--- /dev/null
+++ b/tool/tsh/common/git_config.go
@@ -0,0 +1,184 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package common
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/alecthomas/kingpin/v2"
+ "github.com/gravitational/trace"
+)
+
+// gitConfigCommand implements `tsh git config`.
+//
+// This command internally executes `git` commands like `git config xxx`.
+// can generally assume the user has `git` binary installed (otherwise there is
+// no point using the `git` proxy feature).
+//
+// TODO(greedy52) investigate using `go-git` library instead of calling `git
+// config`.
+type gitConfigCommand struct {
+ *kingpin.CmdClause
+
+ action string
+}
+
+const (
+ gitConfigActionDefault = ""
+ gitConfigActionUpdate = "update"
+ gitConfigActionReset = "reset"
+
+ // gitCoreSSHCommand is the Git config used for setting up alternative SSH
+ // command. For Git-proxying, the command should point to "tsh git ssh".
+ //
+ // https://git-scm.com/docs/git-config#Documentation/git-config.txt-coresshCommand
+ gitCoreSSHCommand = "core.sshcommand"
+)
+
+func newGitConfigCommand(parent *kingpin.CmdClause) *gitConfigCommand {
+ cmd := &gitConfigCommand{
+ CmdClause: parent.Command("config", "Check Teleport config on the working Git directory. Or provide an action ('update' or 'reset') to configure the Git repo."),
+ }
+
+ cmd.Arg("action", "Optional action to perform. 'update' to configure the Git repo to proxy Git commands through Teleport. 'reset' to clear Teleport configuration from the Git repo.").
+ EnumVar(&cmd.action, gitConfigActionUpdate, gitConfigActionReset)
+ return cmd
+}
+
+func (c *gitConfigCommand) run(cf *CLIConf) error {
+ // Make sure we are in a Git dir.
+ err := execGitWithStdoutAndStderr(cf, io.Discard, io.Discard, "rev-parse", "--is-inside-work-tree")
+ if err != nil {
+ // In case git is not found, return the look path error.
+ if trace.IsNotFound(err) {
+ return trace.Wrap(err)
+ }
+ // This error message is a slight alternation of the original error
+ // message from the above command.
+ return trace.BadParameter("the current directory is not a Git repository (or any of the parent directories)")
+ }
+
+ switch c.action {
+ case gitConfigActionDefault:
+ return trace.Wrap(c.doCheck(cf))
+ case gitConfigActionUpdate:
+ return trace.Wrap(c.doUpdate(cf))
+ case gitConfigActionReset:
+ return trace.Wrap(c.doReset(cf))
+ default:
+ return trace.BadParameter("unknown action '%v'", c.action)
+ }
+}
+
+func (c *gitConfigCommand) doCheck(cf *CLIConf) error {
+ sshCommand, err := c.getCoreSSHCommand(cf)
+ if err != nil {
+ return trace.Wrap(err)
+ }
+ wantPrefix := makeGitCoreSSHCommand(cf.executablePath, "")
+ if strings.HasPrefix(sshCommand, wantPrefix) {
+ _, org, _ := strings.Cut(sshCommand, wantPrefix)
+ fmt.Fprintf(cf.Stdout(), "The current Git directory is configured with Teleport for GitHub organization %q.\n", org)
+ return nil
+ }
+
+ c.printDirNotConfigured(cf.Stdout(), true, sshCommand)
+ return nil
+}
+
+func (c *gitConfigCommand) printDirNotConfigured(w io.Writer, withUpdate bool, existingSSHCommand string) {
+ fmt.Fprintln(w, "The current Git directory is not configured with Teleport.")
+ if withUpdate {
+ if existingSSHCommand != "" {
+ fmt.Fprintf(w, "%q currently has value %q.\n", gitCoreSSHCommand, existingSSHCommand)
+ fmt.Fprintf(w, "Run 'tsh git config update' to configure Git directory with Teleport but %q will be overwritten.\n", gitCoreSSHCommand)
+ } else {
+ fmt.Fprintln(w, "Run 'tsh git config update' to configure it.")
+ }
+ }
+}
+
+func (c *gitConfigCommand) doUpdate(cf *CLIConf) error {
+ urls, err := execGitAndCaptureStdout(cf, "ls-remote", "--get-url")
+ if err != nil {
+ return trace.Wrap(err)
+ }
+ for _, url := range strings.Split(urls, "\n") {
+ u, err := parseGitSSHURL(url)
+ if err != nil {
+ logger.DebugContext(cf.Context, "Skippig URL", "error", err, "url", url)
+ continue
+ }
+ if !u.isGitHub() {
+ logger.DebugContext(cf.Context, "Skippig non-GitHub host", "host", u.Host)
+ continue
+ }
+
+ logger.DebugContext(cf.Context, "Configuring repo to use tsh.", "url", url, "owner", u.owner())
+ args := []string{
+ "config", "--local",
+ "--replace-all", gitCoreSSHCommand,
+ makeGitCoreSSHCommand(cf.executablePath, u.owner()),
+ }
+ if err := execGit(cf, args...); err != nil {
+ return trace.Wrap(err)
+ }
+ fmt.Fprintln(cf.Stdout(), "Teleport configuration added.")
+ return trace.Wrap(c.doCheck(cf))
+ }
+ return trace.NotFound("no GitHub SSH URL found from 'git ls-remote --get-url'")
+}
+
+func (c *gitConfigCommand) doReset(cf *CLIConf) error {
+ sshCommand, err := c.getCoreSSHCommand(cf)
+ if err != nil {
+ return trace.Wrap(err)
+ }
+ wantPrefix := makeGitCoreSSHCommand(cf.executablePath, "")
+ if !strings.HasPrefix(sshCommand, wantPrefix) {
+ c.printDirNotConfigured(cf.Stdout(), false, sshCommand)
+ return nil
+ }
+
+ if err := execGit(cf, "config", "--local", "--unset-all", gitCoreSSHCommand); err != nil {
+ return trace.Wrap(err)
+ }
+ fmt.Fprintln(cf.Stdout(), "Teleport configuration removed.")
+ return nil
+}
+
+func (c *gitConfigCommand) getCoreSSHCommand(cf *CLIConf) (string, error) {
+ return execGitAndCaptureStdout(cf,
+ "config", "--local",
+ // set default to empty to avoid non-zero exit when config is missing
+ "--default", "",
+ "--get", gitCoreSSHCommand,
+ )
+}
+
+// makeGitCoreSSHCommand generates the value for Git config "core.sshcommand".
+func makeGitCoreSSHCommand(tshBin, githubOrg string) string {
+ // Quote the path in case it has spaces
+ return fmt.Sprintf("\"%s\" git ssh --github-org %s",
+ tshBin,
+ githubOrg,
+ )
+}
diff --git a/tool/tsh/common/git_config_test.go b/tool/tsh/common/git_config_test.go
new file mode 100644
index 0000000000000..b045e9342bb5f
--- /dev/null
+++ b/tool/tsh/common/git_config_test.go
@@ -0,0 +1,184 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package common
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os/exec"
+ "slices"
+ "testing"
+
+ "github.com/gravitational/trace"
+ "github.com/stretchr/testify/require"
+)
+
+func isGitDirCheck(cmd *exec.Cmd) bool {
+ return slices.Equal([]string{"git", "rev-parse", "--is-inside-work-tree"}, cmd.Args)
+}
+func isGitListRemoteURL(cmd *exec.Cmd) bool {
+ return slices.Equal([]string{"git", "ls-remote", "--get-url"}, cmd.Args)
+}
+func isGitConfigGetCoreSSHCommand(cmd *exec.Cmd) bool {
+ return slices.Equal([]string{"git", "config", "--local", "--default", "", "--get", "core.sshcommand"}, cmd.Args)
+}
+
+type fakeGitCommandRunner struct {
+ dirCheckError error
+ coreSSHCommand string
+ remoteURL string
+ verifyCommand func(cmd *exec.Cmd) error
+}
+
+func (f fakeGitCommandRunner) run(cmd *exec.Cmd) error {
+ switch {
+ case isGitDirCheck(cmd):
+ return f.dirCheckError
+ case isGitConfigGetCoreSSHCommand(cmd):
+ fmt.Fprintln(cmd.Stdout, f.coreSSHCommand)
+ return nil
+ case isGitListRemoteURL(cmd):
+ fmt.Fprintln(cmd.Stdout, f.remoteURL)
+ return nil
+ default:
+ if f.verifyCommand != nil {
+ return trace.Wrap(f.verifyCommand(cmd))
+ }
+ return trace.NotFound("unknown command")
+ }
+}
+
+func TestGitConfigCommand(t *testing.T) {
+ tests := []struct {
+ name string
+ cmd *gitConfigCommand
+ fakeRunner fakeGitCommandRunner
+ checkError require.ErrorAssertionFunc
+ checkOutputContains string
+ }{
+ {
+ name: "not a git dir",
+ cmd: &gitConfigCommand{},
+ fakeRunner: fakeGitCommandRunner{
+ dirCheckError: trace.BadParameter("not a git dir"),
+ },
+ checkError: func(t require.TestingT, err error, i ...interface{}) {
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "the current directory is not a Git repository")
+ },
+ },
+ {
+ name: "check",
+ cmd: &gitConfigCommand{},
+ fakeRunner: fakeGitCommandRunner{
+ coreSSHCommand: makeGitCoreSSHCommand("tsh", "org"),
+ },
+ checkError: require.NoError,
+ checkOutputContains: "is configured with Teleport for GitHub organization \"org\"",
+ },
+ {
+ name: "check not configured",
+ cmd: &gitConfigCommand{},
+ fakeRunner: fakeGitCommandRunner{
+ coreSSHCommand: "",
+ },
+ checkError: require.NoError,
+ checkOutputContains: "is not configured",
+ },
+ {
+ name: "update success",
+ cmd: &gitConfigCommand{
+ action: gitConfigActionUpdate,
+ },
+ fakeRunner: fakeGitCommandRunner{
+ coreSSHCommand: makeGitCoreSSHCommand("tsh", "org"),
+ remoteURL: "git@github.com:gravitational/teleport.git",
+ verifyCommand: func(cmd *exec.Cmd) error {
+ expect := []string{
+ "git", "config", "--local",
+ "--replace-all", "core.sshcommand",
+ "\"tsh\" git ssh --github-org gravitational",
+ }
+ if !slices.Equal(expect, cmd.Args) {
+ return trace.CompareFailed("expect %v but got %v", expect, cmd.Args)
+ }
+ return nil
+ },
+ },
+ checkError: require.NoError,
+ },
+ {
+ name: "update failed missing url",
+ cmd: &gitConfigCommand{
+ action: gitConfigActionUpdate,
+ },
+ fakeRunner: fakeGitCommandRunner{
+ coreSSHCommand: makeGitCoreSSHCommand("tsh", "org"),
+ remoteURL: "",
+ },
+ checkError: require.Error,
+ },
+ {
+ name: "reset no-op",
+ cmd: &gitConfigCommand{
+ action: gitConfigActionReset,
+ },
+ fakeRunner: fakeGitCommandRunner{
+ coreSSHCommand: "",
+ },
+ checkError: require.NoError,
+ },
+ {
+ name: "reset no-op",
+ cmd: &gitConfigCommand{
+ action: gitConfigActionReset,
+ },
+ fakeRunner: fakeGitCommandRunner{
+ coreSSHCommand: makeGitCoreSSHCommand("tsh", "org"),
+ verifyCommand: func(cmd *exec.Cmd) error {
+ expect := []string{
+ "git", "config", "--local",
+ "--unset-all", "core.sshcommand",
+ }
+ if !slices.Equal(expect, cmd.Args) {
+ return trace.CompareFailed("expect %v but got %v", expect, cmd.Args)
+ }
+ return nil
+ },
+ },
+ checkError: require.NoError,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ cf := &CLIConf{
+ Context: context.Background(),
+ OverrideStdout: &buf,
+ executablePath: "tsh",
+ cmdRunner: tt.fakeRunner.run,
+ lookPathOverride: "git",
+ }
+ tt.checkError(t, tt.cmd.run(cf))
+ require.Contains(t, buf.String(), tt.checkOutputContains)
+ })
+ }
+}
diff --git a/tool/tsh/common/git_ssh.go b/tool/tsh/common/git_ssh.go
new file mode 100644
index 0000000000000..d4221d0f2f286
--- /dev/null
+++ b/tool/tsh/common/git_ssh.go
@@ -0,0 +1,86 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package common
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/alecthomas/kingpin/v2"
+ "github.com/gravitational/trace"
+
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/client"
+)
+
+// gitSSHCommand implements `tsh git ssh`.
+//
+// Note that this is a hidden command as it is only meant for 'git` to call.
+// TODO(greedy52) support Git protocol v2.
+type gitSSHCommand struct {
+ *kingpin.CmdClause
+
+ gitHubOrg string
+ userHost string
+ command []string
+ options []string
+}
+
+func newGitSSHCommand(parent *kingpin.CmdClause) *gitSSHCommand {
+ cmd := &gitSSHCommand{
+ CmdClause: parent.Command("ssh", "Proxy Git commands using SSH").Hidden(),
+ }
+
+ cmd.Flag("github-org", "GitHub organization.").Required().StringVar(&cmd.gitHubOrg)
+ cmd.Arg("[user@]host", "Remote hostname and the login to use").Required().StringVar(&cmd.userHost)
+ cmd.Arg("command", "Command to execute on a remote host").StringsVar(&cmd.command)
+ cmd.Flag("option", "OpenSSH options in the format used in the configuration file").Short('o').AllowDuplicate().StringsVar(&cmd.options)
+ return cmd
+}
+
+func (c *gitSSHCommand) run(cf *CLIConf) error {
+ _, host, ok := strings.Cut(c.userHost, "@")
+ if !ok || host != "github.com" {
+ return trace.BadParameter("user-host %q is not GitHub", c.userHost)
+ }
+
+ // TODO(greedy52) when git calls tsh, tsh cannot prompt for password (e.g.
+ // user session expired) using provided stdin pipe. `tc.Login` should try
+ // hijacking "/dev/tty" and replace `prompt.Stdin` temporarily.
+ identity, err := getGitHubIdentity(cf, c.gitHubOrg)
+ if err != nil {
+ return trace.Wrap(err)
+ }
+ logger.DebugContext(cf.Context, "Proxying git command for GitHub user.", "command", c.command, "user", identity.Username)
+
+ cf.RemoteCommand = c.command
+ cf.Options = c.options
+ cf.UserHost = fmt.Sprintf("git@%s", types.MakeGitHubOrgServerDomain(c.gitHubOrg))
+
+ tc, err := makeClient(cf)
+ if err != nil {
+ return trace.Wrap(err)
+ }
+ tc.Stdin = os.Stdin
+ err = client.RetryWithRelogin(cf.Context, tc, func() error {
+ return tc.SSH(cf.Context, cf.RemoteCommand)
+ })
+ return trace.Wrap(convertSSHExitCode(tc, err))
+}
diff --git a/tool/tsh/common/git_test.go b/tool/tsh/common/git_test.go
new file mode 100644
index 0000000000000..501004abd141d
--- /dev/null
+++ b/tool/tsh/common/git_test.go
@@ -0,0 +1,79 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package common
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func Test_parseGitSSHURL(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ wantError bool
+ wantOut *gitSSHURL
+ }{
+ {
+ name: "github ssh format",
+ input: "org-1234567@github.com:some-org/some-repo.git",
+ wantOut: &gitSSHURL{
+ Protocol: "ssh",
+ Host: "github.com",
+ User: "org-1234567",
+ Path: "some-org/some-repo.git",
+ Port: 22,
+ },
+ },
+ {
+ name: "github ssh format invalid path",
+ input: "org-1234567@github.com:missing-org",
+ wantError: true,
+ },
+ {
+ name: "ssh schema format",
+ input: "ssh://git@github.com/some-org/some-repo.git",
+ wantOut: &gitSSHURL{
+ Protocol: "ssh",
+ Host: "github.com",
+ User: "git",
+ Path: "/some-org/some-repo.git",
+ },
+ },
+ {
+ name: "unsupported format",
+ input: "https://github.com/gravitational/teleport.git",
+ wantError: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.input, func(t *testing.T) {
+ out, err := parseGitSSHURL(tt.input)
+ t.Log(out, err)
+ if tt.wantError {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ require.Equal(t, tt.wantOut, out)
+ })
+ }
+}
diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go
index d65af0b7247db..7677a6a842251 100644
--- a/tool/tsh/common/tsh.go
+++ b/tool/tsh/common/tsh.go
@@ -575,6 +575,9 @@ type CLIConf struct {
// profileStatusOverride overrides return of ProfileStatus(). used in tests.
profileStatusOverride *client.ProfileStatus
+
+ // lookPathOverride overrides return of LookPath(). used in tests.
+ lookPathOverride string
}
// Stdout returns the stdout writer.
@@ -614,6 +617,14 @@ func (c *CLIConf) RunCommand(cmd *exec.Cmd) error {
return trace.Wrap(cmd.Run())
}
+// LookPath searches for an executable named file.
+func (c *CLIConf) LookPath(file string) (string, error) {
+ if c.lookPathOverride != "" {
+ return c.lookPathOverride, nil
+ }
+ return exec.LookPath(file)
+}
+
func Main() {
cmdLineOrig := os.Args[1:]
var cmdLine []string
@@ -1637,6 +1648,12 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
err = gitCmd.list.run(&cf)
case gitCmd.login.FullCommand():
err = gitCmd.login.run(&cf)
+ case gitCmd.ssh.FullCommand():
+ err = gitCmd.ssh.run(&cf)
+ case gitCmd.config.FullCommand():
+ err = gitCmd.config.run(&cf)
+ case gitCmd.clone.FullCommand():
+ err = gitCmd.clone.run(&cf)
default:
// Handle commands that might not be available.
switch {
@@ -3969,7 +3986,12 @@ func onSSH(cf *CLIConf) error {
accessRequestForSSH,
fmt.Sprintf("%s@%s", tc.HostLogin, tc.Host),
)
+
// Exit with the same exit status as the failed command.
+ return trace.Wrap(convertSSHExitCode(tc, err))
+}
+
+func convertSSHExitCode(tc *client.TeleportClient, err error) error {
if tc.ExitStatus != 0 {
var exitErr *common.ExitCodeError
if errors.As(err, &exitErr) {