Skip to content

Commit 6d58c65

Browse files
committed
feat(PVO11Y-4936): Add initial implementation of registry exporter
1 parent dc220cd commit 6d58c65

File tree

5 files changed

+298
-14
lines changed

5 files changed

+298
-14
lines changed

Dockerfile

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ RUN cd exporters && \
2828
fi \
2929
done
3030

31-
32-
FROM registry.access.redhat.com/ubi9-micro@sha256:f5c5213d2969b7b11a6666fc4b849d56b48d9d7979b60a37bb853dff0255c14b
31+
FROM registry.access.redhat.com/ubi9-minimal@sha256:7c5495d5fad59aaee12abc3cbbd2b283818ee1e814b00dbc7f25bf2d14fa4f0c
3332

3433
# Copy all compiled binaries from the builder stage to the final image.
3534
COPY --from=builder /tmp/built_exporters/* /bin/
3635

36+
RUN microdnf install -y podman && microdnf clean all
37+
3738
# Copy the entrypoint script and ensure it's executable.
3839
COPY exporter-build-scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
3940
RUN chmod +x /usr/local/bin/entrypoint.sh
@@ -51,4 +52,4 @@ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
5152

5253
# CMD is empty. The user must specify the exporter name as the first argument
5354
# to the entrypoint when running the container.
54-
CMD []
55+
CMD []
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
This will be documentation for registry monitoring exporter (monitoring for quay, images.paas, etc.), explanations of the modules, background information, and implementation details.
2+
3+
Still subject to name changes...
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
// Package main implements a registry exporter for Prometheus metrics.
2+
package main
3+
4+
import (
5+
"encoding/base64"
6+
"encoding/json"
7+
"log"
8+
"net/http"
9+
"os"
10+
"os/exec"
11+
"strings"
12+
13+
"github.com/prometheus/client_golang/prometheus"
14+
"github.com/prometheus/client_golang/prometheus/promhttp"
15+
"github.com/stretchr/testify/assert/yaml"
16+
)
17+
18+
type Metrics struct {
19+
RegistryTestUp *prometheus.GaugeVec
20+
RegistryPullCount *prometheus.CounterVec
21+
RegistryTotalPullCount *prometheus.CounterVec
22+
RegistryPushCount *prometheus.CounterVec
23+
RegistryTotalPushCount *prometheus.CounterVec
24+
}
25+
26+
// authBase64 is used to parse the "auth" field from docker config JSON.
27+
type authBase64 struct {
28+
Auth string `json:"auth"`
29+
}
30+
31+
// ConfigJSON represents the structure of docker config JSON.
32+
type ConfigJSON struct {
33+
Auths map[string]authBase64 `json:"auths"`
34+
}
35+
36+
// Secret represents a Kubernetes secret containing docker config.
37+
type Secret struct {
38+
Data map[string]string `yaml:"data"`
39+
}
40+
41+
func InitPodmanLogin(registryType string) {
42+
log.Print("Try logging into registry...")
43+
44+
filePath := os.Getenv("DOCKERCFG_PATH")
45+
if filePath == "" {
46+
log.Panicf("DOCKERCFG_PATH environment variable is not set")
47+
return
48+
}
49+
50+
var secret Secret
51+
fileBytes, err := os.ReadFile(filePath)
52+
if err != nil {
53+
log.Printf("failed to read docker config file at %s: %v", filePath, err)
54+
return
55+
}
56+
57+
if err := yaml.Unmarshal(fileBytes, &secret); err != nil {
58+
log.Printf("failed to unmarshal docker config yaml: %v", err)
59+
return
60+
}
61+
62+
dockerConfigB64, ok := secret.Data[".dockerconfigjson"]
63+
if !ok {
64+
log.Printf(".dockerconfigjson not found in secret data")
65+
return
66+
}
67+
68+
dockerConfigJSONBytes, err := base64.StdEncoding.DecodeString(dockerConfigB64)
69+
if err != nil {
70+
log.Printf("failed to decode .dockerconfigjson: %v", err)
71+
return
72+
}
73+
// Note: At this point, this is how would the usual podman authfile look like, maybe it can be used directly?
74+
// ...
75+
76+
var configJSON ConfigJSON
77+
if err := json.Unmarshal(dockerConfigJSONBytes, &configJSON); err != nil {
78+
log.Printf("failed to unmarshal docker config JSON: %v", err)
79+
return
80+
}
81+
82+
// Extract the first auth token found in the file
83+
// Is there a better way for that? :thinking:
84+
var registryAuthToken string
85+
for _, auth := range configJSON.Auths {
86+
registryAuthToken = auth.Auth
87+
break
88+
}
89+
if registryAuthToken == "" {
90+
log.Printf("auth token not found in the docker config file")
91+
return
92+
}
93+
94+
decodedAuth, err := base64.StdEncoding.DecodeString(registryAuthToken)
95+
if err != nil {
96+
log.Printf("failed to decode registry auth token: %v", err)
97+
return
98+
}
99+
100+
decodedAuthParts := strings.SplitN(string(decodedAuth), ":", 2)
101+
if len(decodedAuthParts) != 2 {
102+
log.Printf("invalid registry auth token format")
103+
return
104+
}
105+
106+
loginCmd := exec.Command("podman", "login", "--username", decodedAuthParts[0], "--password", decodedAuthParts[1], registryType)
107+
loginOutput, loginErr := loginCmd.CombinedOutput()
108+
if loginErr != nil {
109+
log.Panicf("Registry login failed: %v, output: %s", loginErr, string(loginOutput))
110+
}
111+
log.Print("Registry login successful.")
112+
}
113+
114+
// InitMetrics initializes and registers Prometheus metrics.
115+
func InitMetrics(reg prometheus.Registerer) *Metrics {
116+
m := &Metrics{
117+
RegistryTestUp: prometheus.NewGaugeVec(
118+
prometheus.GaugeOpts{
119+
Name: "registry_test_up",
120+
Help: "A simple gauge to indicate if the registryType is accessible (1 for up).",
121+
},
122+
[]string{"registryType"},
123+
),
124+
RegistryPullCount: prometheus.NewCounterVec(
125+
prometheus.CounterOpts{
126+
Name: "registry_successful_pull_count",
127+
Help: "Total number of successful image pulls.",
128+
},
129+
[]string{"registryType"},
130+
),
131+
RegistryTotalPullCount: prometheus.NewCounterVec(
132+
prometheus.CounterOpts{
133+
Name: "registry_total_pull_count",
134+
Help: "Total number of image pulls.",
135+
},
136+
[]string{"registryType"},
137+
),
138+
RegistryPushCount: prometheus.NewCounterVec(
139+
prometheus.CounterOpts{
140+
Name: "registry_successful_push_count",
141+
Help: "Total number of successful image pushes.",
142+
},
143+
[]string{"registryType"},
144+
),
145+
RegistryTotalPushCount: prometheus.NewCounterVec(
146+
prometheus.CounterOpts{
147+
Name: "registry_total_push_count",
148+
Help: "Total number of image pushes.",
149+
},
150+
[]string{"registryType"},
151+
),
152+
}
153+
reg.MustRegister(m.RegistryTestUp)
154+
reg.MustRegister(m.RegistryPullCount)
155+
reg.MustRegister(m.RegistryTotalPullCount)
156+
reg.MustRegister(m.RegistryPushCount)
157+
reg.MustRegister(m.RegistryTotalPushCount)
158+
return m
159+
}
160+
161+
var registryTypes = map[string]string{
162+
"quay.io": "quay.io/redhat-user-workloads/rh-ee-tbehal-tenant/test-component",
163+
"images.paas.redhat.com": "images.paas.redhat.com/o11y/todo",
164+
}
165+
166+
func ImagePullTest(metrics *Metrics, registryType string) {
167+
defer metrics.RegistryTotalPullCount.WithLabelValues(registryType).Inc()
168+
169+
imageName, ok := registryTypes[registryType]
170+
if !ok {
171+
log.Printf("Unknown registry type: %s", registryType)
172+
return
173+
}
174+
imageName += ":pull" // TODO: Add tag management
175+
log.Print("Starting Image Pull Test...")
176+
cmd := exec.Command("podman", "pull", imageName)
177+
output, err := cmd.CombinedOutput()
178+
if err != nil {
179+
log.Printf("Image pull failed: %v, output: %s", err, string(output))
180+
return
181+
}
182+
log.Print("Image pull successful.")
183+
metrics.RegistryPullCount.WithLabelValues(registryType).Inc()
184+
}
185+
186+
// TODO: This works only locally, probably needs to be adapted for cluster use
187+
func CreateDockerfile() {
188+
dockerfileContent := `FROM busybox:glibc
189+
190+
# Add a build-time timestamp to force uniqueness and avoid layer caching
191+
ARG BUILD_TIMESTAMP
192+
ENV BUILD_TIMESTAMP=${BUILD_TIMESTAMP}
193+
194+
LABEL quay.expires-after="1m"
195+
196+
# Example: touch a file with the timestamp
197+
RUN echo "${BUILD_TIMESTAMP}" > /timestamp.txt
198+
`
199+
200+
// PVC mountpoint expected e.g.: /mnt/data/
201+
filePath := os.Getenv("DOCKERFILE_PATH")
202+
if filePath == "" {
203+
log.Panicf("DOCKERFILE_PATH environment variable is not set")
204+
return
205+
}
206+
207+
if err := os.WriteFile(filePath+"Dockerfile", []byte(dockerfileContent), 0644); err != nil {
208+
log.Panicf("Failed to create Dockerfile: %v", err)
209+
}
210+
}
211+
212+
func ImagePushTest(metrics *Metrics, registryType string) {
213+
defer metrics.RegistryTotalPushCount.WithLabelValues(registryType).Inc()
214+
215+
imageName, ok := registryTypes[registryType]
216+
if !ok {
217+
log.Printf("Unknown registry type: %s", registryType)
218+
return
219+
}
220+
imageName += ":push" // TODO: Add tag management
221+
log.Print("Starting Image Push Test...")
222+
223+
// Build image with podman
224+
buildTimestamp := os.Getenv("BUILD_TIMESTAMP")
225+
if buildTimestamp == "" {
226+
buildTimestamp = "now"
227+
}
228+
buildCmd := exec.Command("podman", "build", "-t", imageName, "--build-arg", "BUILD_TIMESTAMP="+buildTimestamp, "-f", os.Getenv("DOCKERFILE_PATH")+"Dockerfile", ".")
229+
buildOutput, buildErr := buildCmd.CombinedOutput()
230+
if buildErr != nil {
231+
log.Printf("Image build failed: %v, output: %s", buildErr, string(buildOutput))
232+
return
233+
}
234+
log.Print("Image build successful.")
235+
236+
// For push, assume podman login is handled externally or via entrypoint script
237+
pushCmd := exec.Command("podman", "push", imageName)
238+
pushOutput, pushErr := pushCmd.CombinedOutput()
239+
if pushErr != nil {
240+
log.Printf("Image push failed: %v, output: %s", pushErr, string(pushOutput))
241+
return
242+
}
243+
log.Print("Image push successful.")
244+
metrics.RegistryPushCount.WithLabelValues(registryType).Inc()
245+
}
246+
247+
func ImageManifestTest(metrics *Metrics) {
248+
// TODO: Implement manifest test
249+
}
250+
251+
func main() {
252+
log.SetOutput(os.Stderr)
253+
254+
registryType := "quay.io"
255+
256+
reg := prometheus.NewRegistry()
257+
metrics := InitMetrics(reg)
258+
259+
InitPodmanLogin(registryType)
260+
CreateDockerfile()
261+
262+
metrics.RegistryTestUp.WithLabelValues(registryType).Set(1)
263+
264+
handler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
265+
266+
http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
267+
log.Println("Metrics endpoint hit, running tests...")
268+
go ImagePullTest(metrics, registryType)
269+
go ImagePushTest(metrics, registryType)
270+
handler.ServeHTTP(w, r)
271+
272+
// TODO: Figure out where to increment these
273+
// metrics.RegistryTotalPullCount.WithLabelValues(registryType).Inc()
274+
// metrics.RegistryTotalPushCount.WithLabelValues(registryType).Inc()
275+
})
276+
277+
log.Println("http://localhost:9101/metrics")
278+
log.Fatal(http.ListenAndServe(":9101", nil))
279+
}

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,20 @@ require (
3434
github.com/josharian/intern v1.0.0 // indirect
3535
github.com/json-iterator/go v1.1.12 // indirect
3636
github.com/kylelemons/godebug v1.1.0 // indirect
37-
github.com/mailru/easyjson v0.9.0 // indirect
37+
github.com/mailru/easyjson v0.9.1 // indirect
3838
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3939
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
4040
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
41+
github.com/rogpeppe/go-internal v1.14.1 // indirect
4142
github.com/spf13/pflag v1.0.10 // indirect
4243
github.com/x448/float16 v0.8.4 // indirect
4344
go.yaml.in/yaml/v2 v2.4.3 // indirect
4445
go.yaml.in/yaml/v3 v3.0.4 // indirect
4546
golang.org/x/net v0.44.0 // indirect
46-
golang.org/x/oauth2 v0.30.0 // indirect
47+
golang.org/x/oauth2 v0.31.0 // indirect
4748
golang.org/x/term v0.35.0 // indirect
4849
golang.org/x/text v0.29.0 // indirect
49-
golang.org/x/time v0.12.0 // indirect
50+
golang.org/x/time v0.13.0 // indirect
5051
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
5152
gopkg.in/inf.v0 v0.9.1 // indirect
5253
k8s.io/api v0.34.1 // indirect

go.sum

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
7474
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
7575
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
7676
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
77-
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
78-
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
77+
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
78+
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
7979
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
8080
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
8181
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -99,8 +99,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z
9999
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
100100
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
101101
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
102-
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
103-
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
102+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
103+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
104104
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
105105
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
106106
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -134,8 +134,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
134134
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
135135
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
136136
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
137-
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
138-
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
137+
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
138+
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
139139
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
140140
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
141141
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -150,8 +150,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
150150
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
151151
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
152152
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
153-
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
154-
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
153+
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
154+
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
155155
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
156156
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
157157
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=

0 commit comments

Comments
 (0)