Skip to content

Commit d711543

Browse files
author
Paul Miller
authored
Merge pull request #7 from Liunardy/kind-e2e-test
Add kind e2e test
2 parents 0488ad3 + f8baf82 commit d711543

9 files changed

Lines changed: 279 additions & 10 deletions

File tree

.github/workflows/e2e-tests.yaml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: End-to-End Tests
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
jobs:
10+
e2e-test:
11+
name: E2E Tests
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
18+
- name: Setup Go
19+
uses: actions/setup-go@v5
20+
with:
21+
go-version: "1.24.3"
22+
23+
- name: Setup kubectl
24+
uses: azure/setup-kubectl@v4
25+
26+
- name: Install kind
27+
run: go install sigs.k8s.io/kind@v0.29.0
28+
29+
- name: Install Kustomize
30+
run: |
31+
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
32+
sudo mv kustomize /usr/local/bin/
33+
34+
- name: Run E2E tests
35+
run: go test ./test/e2e/ -v -ginkgo.v

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ COPY *.go ./
88
COPY pkg/ ./pkg/
99

1010
RUN go vet -v
11-
RUN go test -v ./...
11+
RUN go test -v $(go list ./... | grep -v /e2e)
1212

1313
RUN go build -o /go/bin/app
1414

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ For streamlined deployment, a `deploy.yaml` file is provided. This file can be a
4545
1. Apply the deployment manifest:
4646

4747
```bash
48-
kubectl apply -f deploy.yaml
48+
kubectl kustomize config/base/ | kubectl apply -f -
4949
```
5050

5151
1. Monitor the deployment and ensure the pods are running:
File renamed without changes.

config/base/kustomization.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
apiVersion: kustomize.config.k8s.io/v1beta1
2+
kind: Kustomization
3+
4+
resources:
5+
- deploy.yaml
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: kustomize.config.k8s.io/v1beta1
2+
kind: Kustomization
3+
4+
resources:
5+
- ../../base/
6+
images:
7+
- name: paulgmiller/corednsprobe
8+
newTag: e2etest

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ go 1.24.3
44

55
require (
66
github.com/alexflint/go-arg v1.5.1
7+
github.com/onsi/ginkgo/v2 v2.23.4
8+
github.com/onsi/gomega v1.37.0
79
github.com/prometheus/client_golang v1.22.0
810
github.com/prometheus/client_model v0.6.2
911
github.com/prometheus/common v0.64.0
@@ -23,9 +25,11 @@ require (
2325
github.com/go-openapi/jsonpointer v0.21.0 // indirect
2426
github.com/go-openapi/jsonreference v0.20.2 // indirect
2527
github.com/go-openapi/swag v0.23.0 // indirect
28+
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
2629
github.com/gogo/protobuf v1.3.2 // indirect
2730
github.com/google/gnostic-models v0.6.9 // indirect
2831
github.com/google/go-cmp v0.7.0 // indirect
32+
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
2933
github.com/google/uuid v1.6.0 // indirect
3034
github.com/josharian/intern v1.0.0 // indirect
3135
github.com/json-iterator/go v1.1.12 // indirect
@@ -37,12 +41,14 @@ require (
3741
github.com/prometheus/procfs v0.15.1 // indirect
3842
github.com/spf13/pflag v1.0.6 // indirect
3943
github.com/x448/float16 v0.8.4 // indirect
44+
go.uber.org/automaxprocs v1.6.0 // indirect
4045
golang.org/x/net v0.40.0 // indirect
4146
golang.org/x/oauth2 v0.30.0 // indirect
4247
golang.org/x/sys v0.33.0 // indirect
4348
golang.org/x/term v0.32.0 // indirect
4449
golang.org/x/text v0.25.0 // indirect
4550
golang.org/x/time v0.9.0 // indirect
51+
golang.org/x/tools v0.31.0 // indirect
4652
google.golang.org/protobuf v1.36.6 // indirect
4753
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
4854
gopkg.in/inf.v0 v0.9.1 // indirect

go.sum

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
3434
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
3535
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
3636
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
37-
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
38-
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
37+
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
38+
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
3939
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
4040
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
4141
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -64,14 +64,16 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
6464
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
6565
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
6666
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
67-
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
68-
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
69-
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
70-
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
67+
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
68+
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
69+
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
70+
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
7171
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
7272
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
7373
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7474
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
75+
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
76+
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
7577
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
7678
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
7779
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
@@ -100,6 +102,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
100102
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
101103
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
102104
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
105+
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
106+
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
103107
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
104108
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
105109
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -133,8 +137,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
133137
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
134138
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
135139
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
136-
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
137-
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
140+
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
141+
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
138142
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
139143
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
140144
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

test/e2e/e2e_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Package e2e contains end-to-end tests for the CoreDNS probe.
2+
package e2e
3+
4+
import (
5+
"context"
6+
"fmt"
7+
"io"
8+
"maps"
9+
"net/http"
10+
"os"
11+
"os/exec"
12+
"path/filepath"
13+
"slices"
14+
"strings"
15+
"testing"
16+
17+
. "github.com/onsi/ginkgo/v2"
18+
. "github.com/onsi/gomega"
19+
"github.com/onsi/gomega/gbytes"
20+
"github.com/onsi/gomega/gexec"
21+
"github.com/prometheus/common/expfmt"
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
"k8s.io/client-go/kubernetes"
24+
"k8s.io/client-go/tools/clientcmd"
25+
)
26+
27+
func TestE2E(t *testing.T) {
28+
RegisterFailHandler(Fail)
29+
RunSpecs(t, "CoreDNS Probe E2E Suite")
30+
}
31+
32+
const (
33+
clusterName = "corednsprobe-test"
34+
namespace = "kube-system"
35+
deploymentName = "coredns-probe"
36+
metricsPort = 9091
37+
probeImage = "paulgmiller/corednsprobe:e2etest"
38+
)
39+
40+
var (
41+
clientset *kubernetes.Clientset
42+
testDir string
43+
corednsIPs map[string]struct{}
44+
)
45+
46+
var _ = BeforeSuite(func() {
47+
// Create a temporary directory for test artifacts.
48+
testDir, err := os.MkdirTemp("", "corednsprobe-e2e-")
49+
Expect(err).NotTo(HaveOccurred())
50+
51+
By("Creating a Kind cluster")
52+
kubeConfigPath := filepath.Join(testDir, "kubeconfig")
53+
os.Setenv("KUBECONFIG", kubeConfigPath)
54+
kindCmd := exec.Command("kind", "create", "cluster", "--name", clusterName, "--kubeconfig", kubeConfigPath)
55+
output, err := kindCmd.CombinedOutput()
56+
Expect(err).NotTo(HaveOccurred(), "Failed to create Kind cluster: %s", string(output))
57+
GinkgoWriter.Println(string(output))
58+
59+
// Initialize Kubernetes client.
60+
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
61+
Expect(err).NotTo(HaveOccurred())
62+
clientset, err = kubernetes.NewForConfig(config)
63+
Expect(err).NotTo(HaveOccurred())
64+
65+
By("Building Docker image for CoreDNS probe")
66+
gitRoot, err := getGitRoot()
67+
Expect(err).NotTo(HaveOccurred(), "Failed to get Git root directory")
68+
buildCmd := exec.Command("docker", "build", "-t", probeImage, gitRoot)
69+
buildOutput, err := buildCmd.CombinedOutput()
70+
Expect(err).NotTo(HaveOccurred(), "Failed to build Docker image: %s", string(buildOutput))
71+
GinkgoWriter.Println(string(buildOutput))
72+
73+
By("Loading Docker image into Kind")
74+
loadCmd := exec.Command("kind", "load", "docker-image", probeImage, "--name", clusterName)
75+
loadOutput, err := loadCmd.CombinedOutput()
76+
Expect(err).NotTo(HaveOccurred(), "Failed to load image into Kind: %s", string(loadOutput))
77+
GinkgoWriter.Println(string(loadOutput))
78+
79+
By("Waiting for CoreDNS pods to be running")
80+
corednsIPs = make(map[string]struct{})
81+
Eventually(func() bool {
82+
podList, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{
83+
LabelSelector: "k8s-app=kube-dns",
84+
})
85+
if err != nil {
86+
return false
87+
}
88+
for _, pod := range podList.Items {
89+
if pod.Status.Phase != "Running" || pod.Status.PodIP == "" {
90+
return false
91+
}
92+
corednsIPs[pod.Status.PodIP] = struct{}{}
93+
}
94+
return len(podList.Items) > 0
95+
}, "180s", "2s").Should(BeTrue(), "CoreDNS pods are not running")
96+
GinkgoWriter.Println("CoreDNS pod IPs:", slices.Collect(maps.Keys(corednsIPs)))
97+
98+
By("Deploying CoreDNS probe")
99+
deployCmdStr := fmt.Sprintf("kustomize edit set image %s && kustomize build . | kubectl apply -f -", probeImage)
100+
deployCmd := exec.Command("bash", "-c", deployCmdStr)
101+
deployCmd.Env = os.Environ()
102+
deployCmd.Dir = filepath.Join(gitRoot, "config", "overlays", "e2e")
103+
deployOutput, err := deployCmd.CombinedOutput()
104+
Expect(err).NotTo(HaveOccurred(), "Failed to deploy CoreDNS probe: %s", string(deployOutput))
105+
GinkgoWriter.Println(string(deployOutput))
106+
107+
By("Waiting for CoreDNS probe deployment to become ready")
108+
Eventually(func() bool {
109+
deployment, err := clientset.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
110+
if err != nil {
111+
return false
112+
}
113+
return deployment.Status.ReadyReplicas == *deployment.Spec.Replicas
114+
}, "90s", "2s").Should(BeTrue())
115+
116+
By("Listing all pods in all namespaces")
117+
podsCmd := exec.Command("kubectl", "get", "po", "-A")
118+
podsCmd.Env = os.Environ()
119+
podsOutput, err := podsCmd.CombinedOutput()
120+
Expect(err).NotTo(HaveOccurred(), "Failed to list pods: %s", string(podsOutput))
121+
GinkgoWriter.Println(string(podsOutput))
122+
})
123+
124+
var _ = AfterSuite(func() {
125+
By("Deleting the Kind cluster")
126+
kindCmd := exec.Command("kind", "delete", "cluster", "--name", clusterName)
127+
kindCmd.CombinedOutput()
128+
129+
os.RemoveAll(testDir)
130+
})
131+
132+
var _ = Describe("CoreDNS Probe deployment", func() {
133+
It("should have the CoreDNS probe pod running", func() {
134+
deployment, err := clientset.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
135+
Expect(err).NotTo(HaveOccurred())
136+
Expect(deployment.Status.AvailableReplicas).To(Equal(*deployment.Spec.Replicas))
137+
138+
podList, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{
139+
LabelSelector: "app=" + deploymentName,
140+
})
141+
Expect(err).NotTo(HaveOccurred())
142+
Expect(podList.Items).NotTo(BeEmpty())
143+
})
144+
145+
It("should expose metrics endpoint", func() {
146+
podList, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{
147+
LabelSelector: "app=" + deploymentName,
148+
})
149+
Expect(err).NotTo(HaveOccurred())
150+
Expect(podList.Items).NotTo(BeEmpty())
151+
152+
pod := podList.Items[0]
153+
154+
By("Port-forwarding to the CoreDNS probe pod")
155+
portForwardCmd := exec.Command("kubectl", "port-forward",
156+
fmt.Sprintf("pod/%s", pod.Name),
157+
fmt.Sprintf("%d:%d", metricsPort, metricsPort),
158+
"-n", namespace)
159+
portForwardCmd.Env = os.Environ()
160+
session, err := gexec.Start(portForwardCmd, GinkgoWriter, GinkgoWriter)
161+
Expect(err).NotTo(HaveOccurred())
162+
defer session.Kill()
163+
164+
By("Waiting for port forwarding to be established")
165+
Eventually(session, "5s", "1s").Should(gbytes.Say("Forwarding from"), "Failed to establish port-forwarding")
166+
167+
By("Checking if metrics endpoint is accessible")
168+
res, err := http.Get(fmt.Sprintf("http://localhost:%d/metrics", metricsPort))
169+
Expect(err).NotTo(HaveOccurred(), "Failed to access metrics endpoint")
170+
defer res.Body.Close()
171+
Expect(res.StatusCode).To(Equal(http.StatusOK), "Metrics endpoint did not return 200 OK")
172+
173+
By("Verifying metrics format")
174+
body, err := io.ReadAll(res.Body)
175+
Expect(err).NotTo(HaveOccurred(), "Failed to read response body")
176+
Expect(body).NotTo(BeEmpty(), "Metrics response body is empty")
177+
var parser expfmt.TextParser
178+
metrics, err := parser.TextToMetricFamilies(strings.NewReader(string(body)))
179+
Expect(err).NotTo(HaveOccurred(), "Failed to parse metrics")
180+
metric := metrics["coredns_probe_rtt_milliseconds"]
181+
Expect(metric).NotTo(BeNil(), "Expected coredns_probe_rtt_milliseconds metric not found")
182+
183+
By("Verifying metrics endpoint labels match CoreDNS IPs")
184+
Expect(corednsIPs).NotTo(BeEmpty(), "No CoreDNS pod IPs were discovered")
185+
metricEndpoints := make(map[string]struct{})
186+
for _, m := range metric.Metric {
187+
for _, label := range m.Label {
188+
if label.GetName() == "endpoint" {
189+
ip := label.GetValue()
190+
_, exists := corednsIPs[ip]
191+
Expect(exists).To(BeTrue(), fmt.Sprintf("Unexpected endpoint in metrics: %s", ip))
192+
metricEndpoints[ip] = struct{}{}
193+
GinkgoWriter.Println("Found metrics for CoreDNS IP:", ip)
194+
break
195+
}
196+
}
197+
}
198+
Expect(maps.Equal(metricEndpoints, corednsIPs)).To(BeTrue(), "Metrics endpoints don't match CoreDNS IPs")
199+
})
200+
})
201+
202+
// getGitRoot retrieves the root directory of the Git repository.
203+
func getGitRoot() (string, error) {
204+
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
205+
output, err := cmd.CombinedOutput()
206+
if err != nil {
207+
return "", fmt.Errorf("failed to get Git root directory: %w", err)
208+
}
209+
210+
return strings.TrimSpace(string(output)), nil
211+
}

0 commit comments

Comments
 (0)