Compare commits

...

729 Commits

Author SHA1 Message Date
Philip Laine
c863fc3621
Bump go.etcd.io/bbolt from 1.4.0 to 1.4.1 (#917) 2025-06-12 16:08:32 +02:00
dependabot[bot]
31ea574b45
Bump go.etcd.io/bbolt from 1.4.0 to 1.4.1
Bumps [go.etcd.io/bbolt](https://github.com/etcd-io/bbolt) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/etcd-io/bbolt/releases)
- [Commits](https://github.com/etcd-io/bbolt/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: go.etcd.io/bbolt
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-12 11:22:47 +00:00
Philip Laine
b82deb0d5b
Bump github.com/pion/interceptor from 0.1.37 to 0.1.39 (#918) 2025-06-12 13:21:24 +02:00
dependabot[bot]
d7f464d2f3
Bump github.com/pion/interceptor from 0.1.37 to 0.1.39
Bumps [github.com/pion/interceptor](https://github.com/pion/interceptor) from 0.1.37 to 0.1.39.
- [Release notes](https://github.com/pion/interceptor/releases)
- [Changelog](https://github.com/pion/interceptor/blob/master/.goreleaser.yml)
- [Commits](https://github.com/pion/interceptor/compare/v0.1.37...v0.1.39)

---
updated-dependencies:
- dependency-name: github.com/pion/interceptor
  dependency-version: 0.1.39
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-12 11:15:43 +00:00
Philip Laine
0f1fc2b99d
Refactor OCI client options and add header configuration (#916) 2025-06-11 11:02:06 +02:00
Philip Laine
e092c5c2b8
Refactor OCI client options and add header configuration
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-06-11 10:57:48 +02:00
Philip Laine
424aa603c9
Fix OCI client header parsing and improve tests (#914) 2025-06-08 15:58:01 +02:00
Philip Laine
c4c467734a
Fix OCI client header parsing and improve tests
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-06-08 15:18:24 +02:00
Philip Laine
94451e5b62
Bump golang.org/x/sync from 0.14.0 to 0.15.0 (#912) 2025-06-07 09:18:43 +02:00
dependabot[bot]
fa6aeb7614
Bump golang.org/x/sync from 0.14.0 to 0.15.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.14.0 to 0.15.0.
- [Commits](https://github.com/golang/sync/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-version: 0.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 17:53:14 +00:00
Philip Laine
33a9b42605
Bump github.com/multiformats/go-multiaddr from 0.15.0 to 0.16.0 (#913) 2025-06-06 19:51:53 +02:00
dependabot[bot]
2977a8b98a
Bump github.com/multiformats/go-multiaddr from 0.15.0 to 0.16.0
Bumps [github.com/multiformats/go-multiaddr](https://github.com/multiformats/go-multiaddr) from 0.15.0 to 0.16.0.
- [Release notes](https://github.com/multiformats/go-multiaddr/releases)
- [Commits](https://github.com/multiformats/go-multiaddr/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: github.com/multiformats/go-multiaddr
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 11:41:20 +00:00
Philip Laine
6b11e88a23
Bump google.golang.org/grpc from 1.72.2 to 1.73.0 (#907) 2025-06-06 10:03:57 +02:00
dependabot[bot]
41745837ec
Bump google.golang.org/grpc from 1.72.2 to 1.73.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.72.2 to 1.73.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.72.2...v1.73.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.73.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 07:08:40 +00:00
Philip Laine
6fcc8f7455
Bump github.com/multiformats/go-multicodec from 0.9.0 to 0.9.1 (#908) 2025-06-06 09:07:06 +02:00
dependabot[bot]
7b2a9030be
Bump github.com/multiformats/go-multicodec from 0.9.0 to 0.9.1
Bumps [github.com/multiformats/go-multicodec](https://github.com/multiformats/go-multicodec) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/multiformats/go-multicodec/releases)
- [Commits](https://github.com/multiformats/go-multicodec/compare/v0.9.0...v0.9.1)

---
updated-dependencies:
- dependency-name: github.com/multiformats/go-multicodec
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-05 15:49:04 +00:00
Philip Laine
7e9e5fcff8
Enforce use of request contexts and fix response closing (#911) 2025-06-05 16:18:37 +02:00
Philip Laine
153e54ecba
Enforce use of request contexts and fix response closing
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-06-05 16:11:25 +02:00
Philip Laine
de24996538
Add drain and close function (#910) 2025-06-05 15:25:56 +02:00
Philip Laine
b56f7baa5c
Add drain and close function
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-06-05 15:15:58 +02:00
Philip Laine
ab4d9a5d4d
Add base http client and transport (#909) 2025-06-05 14:43:26 +02:00
Philip Laine
620dd57d6b
Add base http client and transport
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-06-05 14:33:36 +02:00
Philip Laine
97b8d09503
Replace HTTP header strings with httpx constants (#906) 2025-06-05 11:50:20 +02:00
Philip Laine
51f11bf30b
Replace HTTP header strings with httpx constants
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-06-05 11:43:12 +02:00
Philip Laine
d5b5e9250f
Change mirror type to url and add byte range parameter (#905) 2025-06-05 11:18:40 +02:00
Philip Laine
d1d2627b1d
Change mirror type to url and add byte range parameter
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-06-05 11:12:10 +02:00
Philip Laine
813b785308
Prepare for v0.3.0 release (#904) 2025-06-05 10:00:05 +02:00
Philip Laine
fa14e16c85
Prepare for v0.3.0 release 2025-06-05 09:45:46 +02:00
Philip Laine
deef7bed9d
Set Go version using go toolchain (#903) 2025-06-05 09:45:23 +02:00
Philip Laine
e3bd6b8616
Set Go version using go toolchain
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-06-05 09:39:12 +02:00
Philip Laine
98616ce43f
Disable data dir when running Spegel in Kubernetes (#902) 2025-06-05 09:13:04 +02:00
Philip Laine
2b6491d040
Disable data dir when running Spegel in Kubernetes
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-06-05 09:06:19 +02:00
Philip Laine
27bdaa648d
Bump docker/build-push-action from 6.17.0 to 6.18.0 (#900) 2025-06-05 08:16:30 +02:00
dependabot[bot]
8a00852f8a
Bump docker/build-push-action from 6.17.0 to 6.18.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.17.0 to 6.18.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](1dc7386353...263435318d)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-05 06:03:51 +00:00
Philip Laine
b5e74ebb00
Bump google.golang.org/grpc from 1.72.1 to 1.72.2 (#898) 2025-06-05 08:02:59 +02:00
dependabot[bot]
5adb63626e
Bump google.golang.org/grpc from 1.72.1 to 1.72.2
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.72.1 to 1.72.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.72.1...v1.72.2)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.72.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-04 21:00:19 +00:00
Philip Laine
62126284c1
Bump github.com/go-logr/logr from 1.4.2 to 1.4.3 (#901) 2025-06-04 22:58:52 +02:00
dependabot[bot]
3b28c44e6a
Bump github.com/go-logr/logr from 1.4.2 to 1.4.3
Bumps [github.com/go-logr/logr](https://github.com/go-logr/logr) from 1.4.2 to 1.4.3.
- [Release notes](https://github.com/go-logr/logr/releases)
- [Changelog](https://github.com/go-logr/logr/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-logr/logr/compare/v1.4.2...v1.4.3)

---
updated-dependencies:
- dependency-name: github.com/go-logr/logr
  dependency-version: 1.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-04 12:45:26 +00:00
Philip Laine
3378d97555
fix(dashboard): add Variable for job name (#881) 2025-06-03 10:05:56 +02:00
hofq
2ad98c6cf3 Update CHANGELOG.md 2025-06-03 10:00:56 +02:00
hofq
b141ee6980 fix(dashboard): add gnetId back 2025-06-03 10:00:56 +02:00
hofq
72aabead6f fix(dashboard): remove weird naming 2025-06-03 10:00:56 +02:00
hofq
e55c92fffc fix(dashboard): add Variable for job name 2025-06-03 10:00:56 +02:00
hofq
0cb37c68f6 fix(dashboard): add Variable for job name 2025-06-03 10:00:56 +02:00
Philip Laine
cdff80e41f
Handle situation where digest is missing in reigstry response header (#899) 2025-05-26 20:18:21 +02:00
Philip Laine
61f7d14a92
Handle situation where digest is missing in reigstry response header
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-26 19:16:49 +02:00
Philip Laine
7a2420f7d5
Add descriptor to header conversion (#897) 2025-05-23 14:10:22 +02:00
Philip Laine
16f5d15eab Add descriptor to header conversion
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-23 14:06:20 +02:00
Philip Laine
e816a9ce84
Bump github.com/libp2p/go-libp2p-kad-dht from 0.33.0 to 0.33.1 (#891) 2025-05-23 14:06:07 +02:00
dependabot[bot]
5b93002981
Bump github.com/libp2p/go-libp2p-kad-dht from 0.33.0 to 0.33.1
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.33.0 to 0.33.1.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.33.0...v0.33.1)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-version: 0.33.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-23 11:59:52 +00:00
Philip Laine
969b62c2ad
Bump github.com/containerd/containerd/v2 from 2.1.0 to 2.1.1 (#892) 2025-05-23 13:58:15 +02:00
dependabot[bot]
faca50847a
Bump github.com/containerd/containerd/v2 from 2.1.0 to 2.1.1
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v2.1.0...v2.1.1)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd/v2
  dependency-version: 2.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-23 11:48:19 +00:00
Philip Laine
3472e543a4
Rename package mux to httpx and refactor http helpers (#896) 2025-05-23 13:46:14 +02:00
Philip Laine
fdf96eee4b
Rename package mux to httpx and refactor http helpers
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-23 13:42:22 +02:00
Philip Laine
56c8b000ba
Update Kind to v0.29.0 and Fix Containerd v2 support (#894) 2025-05-22 23:18:20 +02:00
Philip Laine
dbd6e7de85
Update Kind to v0.29.0
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-22 23:08:15 +02:00
Philip Laine
f1cf2530e5
Bump github.com/prometheus/common from 0.63.0 to 0.64.0 (#887) 2025-05-18 20:25:20 +02:00
dependabot[bot]
c902a9c267
Bump github.com/prometheus/common from 0.63.0 to 0.64.0
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.63.0 to 0.64.0.
- [Release notes](https://github.com/prometheus/common/releases)
- [Changelog](https://github.com/prometheus/common/blob/main/RELEASE.md)
- [Commits](https://github.com/prometheus/common/compare/v0.63.0...v0.64.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/common
  dependency-version: 0.64.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-17 09:35:11 +00:00
Philip Laine
33c67a25e1
Bump codecov/codecov-action from 5.4.2 to 5.4.3 (#884) 2025-05-17 11:33:49 +02:00
dependabot[bot]
910a7804f3
Bump codecov/codecov-action from 5.4.2 to 5.4.3
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.2 to 5.4.3.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](ad3126e916...18283e04ce)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-17 09:29:29 +00:00
Philip Laine
2bc7e5f9dd
Bump google.golang.org/grpc from 1.72.0 to 1.72.1 (#883) 2025-05-17 11:14:55 +02:00
dependabot[bot]
acf1e39a84
Bump google.golang.org/grpc from 1.72.0 to 1.72.1
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.72.0 to 1.72.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.72.0...v1.72.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.72.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-17 09:04:29 +00:00
Philip Laine
6769e8b8be
Bump docker/build-push-action from 6.16.0 to 6.17.0 (#885) 2025-05-17 11:02:46 +02:00
dependabot[bot]
bf90eee198
Bump docker/build-push-action from 6.16.0 to 6.17.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.16.0 to 6.17.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](14487ce63c...1dc7386353)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-17 08:51:45 +00:00
Philip Laine
50d69122e0
Refactor Containerd options to use config struct (#890) 2025-05-17 10:36:40 +02:00
Philip Laine
f957bbf5cb
Refactor Containerd options to use config struct
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-17 10:24:25 +02:00
Philip Laine
b79f1f3652
Bump the k8s group with 2 updates (#886) 2025-05-17 10:22:59 +02:00
dependabot[bot]
ce068f9b0c
Bump the k8s group with 2 updates
Bumps the k8s group with 2 updates: [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) and [k8s.io/cri-api](https://github.com/kubernetes/cri-api).


Updates `k8s.io/apimachinery` from 0.33.0 to 0.33.1
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.33.0...v0.33.1)

Updates `k8s.io/cri-api` from 0.33.0 to 0.33.1
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.33.0...v0.33.1)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.33.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
- dependency-name: k8s.io/cri-api
  dependency-version: 0.33.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-16 21:39:05 +00:00
Philip Laine
b09f85f2e4
Add support for content create events (#889) 2025-05-16 23:37:24 +02:00
Philip Laine
5804d44f2f
Add support for content create events
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-16 23:29:37 +02:00
Philip Laine
cb09323784
Refactor OCI events to support content events (#888) 2025-05-16 15:21:56 +02:00
Philip Laine
86c6941fbd
Refactor OCI events to support content events
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-16 15:17:19 +02:00
Philip Laine
6c67bced55
Refactor store advertisement to list content (#880) 2025-05-14 23:35:15 +02:00
Philip Laine
c15997d61b
Refactor store advertisement to list content
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-14 16:33:40 +02:00
Philip Laine
3580214646
Add dial timeout configuration in Containerd mirror configuration (#878) 2025-05-13 23:28:08 +02:00
Philip Laine
7ce1422152
Add dial timeout configuration in Containerd mirror configuration
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-13 23:09:41 +02:00
Philip Laine
137c84254f
Add support for www authenticate header (#877) 2025-05-13 10:00:21 +02:00
Philip Laine
d2045fb7bd
Add support for www authenticate header
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-13 09:54:50 +02:00
Philip Laine
14fd19018c
Add debug logs in the event of e2e test failure (#876) 2025-05-12 14:09:38 +02:00
Philip Laine
5101f38d85
Add debug logs in the event of e2e test failure 2025-05-12 14:01:29 +02:00
Philip Laine
70b87b7378
Bump github.com/containerd/containerd/v2 from 2.0.5 to 2.1.0 (#867) 2025-05-12 13:33:55 +02:00
dependabot[bot]
963bd24372
Bump github.com/containerd/containerd/v2 from 2.0.5 to 2.1.0
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.0.5 to 2.1.0.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v2.0.5...v2.1.0)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd/v2
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 11:29:37 +00:00
Philip Laine
ad3d18774c
Bump github.com/libp2p/go-libp2p-kad-dht from 0.32.0 to 0.33.0 (#870) 2025-05-12 13:27:49 +02:00
dependabot[bot]
cf2ef6c56f
Bump github.com/libp2p/go-libp2p-kad-dht from 0.32.0 to 0.33.0
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.32.0 to 0.33.0.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 07:30:03 +00:00
Philip Laine
c2e27fe353
Change debug unit formatting and add totals (#875) 2025-05-12 09:27:05 +02:00
Philip Laine
957f160944
Change debug unit formatting and add totals
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-11 10:38:49 +02:00
Philip Laine
ceba0fc097
Refactor web to use internal mux router (#873) 2025-05-10 23:43:26 +02:00
Philip Laine
2b38c464d0
Refactor web to use internal mux router
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-10 23:11:53 +02:00
Philip Laine
017e48f251
Implement OCI client and refactor debug web pulling (#871) 2025-05-09 15:58:48 +02:00
Philip Laine
cc15af4ff0
Implement OCI client and refactor debug web pulling
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-09 15:39:05 +02:00
Philip Laine
10f50799d5
Allow returning libp2p crypto priv key in linter (#872) 2025-05-09 15:37:50 +02:00
Philip Laine
875d48cd38
Allow returning libp2p crypto priv key in linter
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-09 15:28:59 +02:00
Philip Laine
1524c7958a
Bump actions/setup-go from 5.4.0 to 5.5.0 (#868) 2025-05-08 17:04:58 +02:00
dependabot[bot]
3014060daf
Bump actions/setup-go from 5.4.0 to 5.5.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.4.0 to 5.5.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](0aaccfd150...d35c59abb0)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 5.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-08 14:42:19 +00:00
Philip Laine
1a467a4611
Bump golang.org/x/sync from 0.13.0 to 0.14.0 (#862) 2025-05-08 16:40:51 +02:00
dependabot[bot]
b46802fd54
Bump golang.org/x/sync from 0.13.0 to 0.14.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.13.0 to 0.14.0.
- [Commits](https://github.com/golang/sync/compare/v0.13.0...v0.14.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-version: 0.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-08 14:35:53 +00:00
Philip Laine
83d0848ca6
Fix request logging for redirects and not found pages (#869) 2025-05-08 16:34:32 +02:00
Philip Laine
c0b0f368eb
Fix request logging for redirects and not found pages
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-08 16:30:17 +02:00
Philip Laine
0641bcea04
Rename OCI client to store (#864) 2025-05-08 13:02:14 +02:00
Philip Laine
83a0a47d40
Rename OCI client to store
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-08 12:58:07 +02:00
Philip Laine
bc42adfd1a
Change kubectl wait logic for conformance job (#865) 2025-05-08 12:57:29 +02:00
Philip Laine
081f3c7462
Change kubectl wait logic for conformance job
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-08 12:52:57 +02:00
Philip Laine
5ca49a9af2
Bump golangci/golangci-lint-action from 7.0.0 to 8.0.0 (#861) 2025-05-05 15:42:55 +02:00
dependabot[bot]
f78e1fadd2
Bump golangci/golangci-lint-action from 7.0.0 to 8.0.0
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7.0.0 to 8.0.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](1481404843...4afd733a84)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 12:11:30 +00:00
Philip Laine
2132ce530c
Bump github.com/libp2p/go-libp2p-kad-dht from 0.31.0 to 0.32.0 (#858) 2025-05-04 16:46:33 +02:00
dependabot[bot]
dbee862eaa
Bump github.com/libp2p/go-libp2p-kad-dht from 0.31.0 to 0.32.0
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.31.0 to 0.32.0.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-04 14:40:28 +00:00
Philip Laine
bbf6afc6e0
Update Go to 1.24.2 (#860) 2025-05-04 16:39:06 +02:00
Philip Laine
9a860b058e
Update Go to 1.24.2
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-05-04 12:11:48 +02:00
Philip Laine
8b31fecc27
Bump oras-project/setup-oras from 1.2.2 to 1.2.3 (#857) 2025-04-29 21:58:27 +02:00
dependabot[bot]
d4cdc013a8
Bump oras-project/setup-oras from 1.2.2 to 1.2.3
Bumps [oras-project/setup-oras](https://github.com/oras-project/setup-oras) from 1.2.2 to 1.2.3.
- [Release notes](https://github.com/oras-project/setup-oras/releases)
- [Commits](5c0b487ce3...8d34698a59)

---
updated-dependencies:
- dependency-name: oras-project/setup-oras
  dependency-version: 1.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-29 19:12:46 +00:00
Philip Laine
004663e228
Refactor mux implementation to make testing easier and enable reusability (#856) 2025-04-29 14:36:11 +02:00
Philip Laine
3f8b690b52
Refactor mux implementation to make testing easier and enable reusability
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-29 14:30:27 +02:00
Philip Laine
23ffe0d7f6
Implement unit tests for cleanup logic (#854) 2025-04-28 09:33:18 +02:00
Philip Laine
d04f97befe
Implement unit tests for cleanup logic
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-27 12:51:15 +02:00
Philip Laine
48b666cb45
Remove use of Afero in Containerd config (#852) 2025-04-27 11:40:50 +02:00
Philip Laine
4849dc00d5
Remove use of Afero in Containerd config
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-27 11:30:29 +02:00
Philip Laine
622a144278
Prepare for v0.2.0 release (#851) 2025-04-26 14:03:47 +02:00
Philip Laine
5a00067ade
Prepare for v0.2.0 release 2025-04-26 13:55:04 +02:00
Philip Laine
8fec4a39df
Persist libp2p key to disk when data directory is set (#850) 2025-04-26 13:53:10 +02:00
Philip Laine
67ecc08279
Perist libp2p key to disk when data directory is set
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-26 13:48:35 +02:00
Philip Laine
4f99026b4e
Fix libp2p options so field is exported in configuration (#849) 2025-04-26 13:41:12 +02:00
Philip Laine
595109b3e3
Fix libp2p options so field is exported in configuration
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-26 12:37:18 +02:00
Philip Laine
66f9aa4d4c
Add support for a static bootstrapper (#848) 2025-04-26 11:57:23 +02:00
Philip Laine
ade0f64250
Add support for a static bootstrapper
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-26 11:43:47 +02:00
Philip Laine
34428192ce
Set default values for address arguments (#847) 2025-04-26 11:41:29 +02:00
Philip Laine
ca0c839c66
Set default values for address arguments
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-26 11:26:30 +02:00
Philip Laine
87a8b553ea
Bump docker/build-push-action from 6.15.0 to 6.16.0 (#845) 2025-04-26 10:43:32 +02:00
dependabot[bot]
decce0c2c2
Bump docker/build-push-action from 6.15.0 to 6.16.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.15.0 to 6.16.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](471d1dc4e0...14487ce63c)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-26 08:39:30 +00:00
Philip Laine
e3b2304320
Build binaries as part of the release process (#846) 2025-04-26 10:38:39 +02:00
Philip Laine
10da718e6b
Build binaries as part of the release process
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-26 10:32:06 +02:00
Philip Laine
8a745a48fc
Fix p2p option naming to conform with the standard (#844) 2025-04-25 11:51:27 +02:00
Philip Laine
19d915debc
Fix p2p option naming to conform with the standard
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-25 11:47:11 +02:00
Philip Laine
7664f5ccd3
Set default workflow permissions where it was missing (#843) 2025-04-25 11:33:04 +02:00
Philip Laine
206a30db43
Set default workflow permissions where it was missing 2025-04-25 11:28:30 +02:00
Philip Laine
0e98ee3e39
Bump the k8s group with 2 updates (#842) 2025-04-25 11:27:07 +02:00
dependabot[bot]
876a6c67ed
Bump the k8s group with 2 updates
Bumps the k8s group with 2 updates: [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) and [k8s.io/cri-api](https://github.com/kubernetes/cri-api).


Updates `k8s.io/apimachinery` from 0.32.4 to 0.33.0
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.32.4...v0.33.0)

Updates `k8s.io/cri-api` from 0.32.4 to 0.33.0
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.32.4...v0.33.0)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: k8s
- dependency-name: k8s.io/cri-api
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: k8s
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-24 11:40:29 +00:00
Philip Laine
ba39b19f99
Bump golang.org/x/net from 0.37.0 to 0.38.0 (#841) 2025-04-24 09:50:11 +02:00
dependabot[bot]
5d9e615a22
Bump golang.org/x/net from 0.37.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-24 07:12:43 +00:00
Philip Laine
ec9a6d311e
Bump github.com/containerd/containerd/v2 from 2.0.4 to 2.0.5 (#834) 2025-04-23 22:22:41 +02:00
dependabot[bot]
fdc2b1ba74
Bump github.com/containerd/containerd/v2 from 2.0.4 to 2.0.5
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.0.4 to 2.0.5.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v2.0.4...v2.0.5)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd/v2
  dependency-version: 2.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 20:09:43 +00:00
Philip Laine
b1d34d65ad
Bump google.golang.org/grpc from 1.71.1 to 1.72.0 (#836) 2025-04-23 22:08:25 +02:00
dependabot[bot]
b6c23bc74e
Bump google.golang.org/grpc from 1.71.1 to 1.72.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.71.1 to 1.72.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.71.1...v1.72.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 20:03:49 +00:00
Philip Laine
5b4ac08929
Bump the k8s group with 2 updates (#839) 2025-04-23 22:02:23 +02:00
dependabot[bot]
f9e39e4f75
Bump the k8s group with 2 updates
Bumps the k8s group with 2 updates: [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) and [k8s.io/cri-api](https://github.com/kubernetes/cri-api).


Updates `k8s.io/apimachinery` from 0.32.3 to 0.32.4
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.32.3...v0.32.4)

Updates `k8s.io/cri-api` from 0.32.3 to 0.32.4
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.32.3...v0.32.4)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.32.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
- dependency-name: k8s.io/cri-api
  dependency-version: 0.32.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 19:51:43 +00:00
Philip Laine
c6309ab983
Bump sigstore/cosign-installer from 3.8.1 to 3.8.2 (#840) 2025-04-23 21:49:53 +02:00
dependabot[bot]
3cfbdef0a0
Bump sigstore/cosign-installer from 3.8.1 to 3.8.2
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.8.1 to 3.8.2.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](d7d6bc7722...3454372f43)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 3.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 11:37:12 +00:00
Philip Laine
48aefe17c3
Refactor registry config to align with router config (#835) 2025-04-19 14:46:39 +02:00
Philip Laine
de7e796082
Refactor registry config to align with router config
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-19 14:41:02 +02:00
Philip Laine
4eae464ac5
Add delete hook to cleanup configuration from host when chart is uninstalled (#832) 2025-04-17 21:57:49 +02:00
Philip Laine
8d58484fd6
Add delete hook to cleanup configuration form host when chart in uninstalled
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-17 20:20:41 +02:00
Philip Laine
c0be5b118d
Remove local address check when resolving peers (#831) 2025-04-15 20:15:20 +02:00
Philip Laine
1168ad84c6
Remove local address check when resolving peers
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-15 20:07:19 +02:00
Philip Laine
7b7cb2f240
Bump codecov/codecov-action from 5.4.0 to 5.4.2 (#828) 2025-04-15 14:20:51 +02:00
dependabot[bot]
f872b3cd02
Bump codecov/codecov-action from 5.4.0 to 5.4.2
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.0 to 5.4.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](0565863a31...ad3126e916)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-15 11:53:03 +00:00
Philip Laine
437d9100a7
Add p2p options to router for optional configuration (#827) 2025-04-15 13:52:03 +02:00
Philip Laine
6b4accdc71
Add p2p options to router for optional configuration
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-15 13:42:58 +02:00
Philip Laine
1d8aaba034
Standardize router channel naming (#826) 2025-04-15 12:18:38 +02:00
Philip Laine
8830db9865
Standardize router channel naming
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-15 12:06:15 +02:00
Philip Laine
7b5ef12a86
Fix gopls modernize warnings (#825) 2025-04-15 11:32:26 +02:00
Philip Laine
e52e1c4682
Fix gopls modernize warnings
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-15 11:22:30 +02:00
Philip Laine
fa5c3bd1c1
Fix improper image string formatting and expand tests (#824) 2025-04-15 10:35:46 +02:00
Philip Laine
db8bce1238
Fix improper image string formatting and expand tests
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-15 10:22:56 +02:00
Philip Laine
2196d88505
Switch to using new test context (#820) 2025-04-14 13:58:41 +02:00
Philip Laine
429eb1c000
Switch to using new test context
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-14 13:50:06 +02:00
Philip Laine
7a9cfde12c
Bump helm.sh/helm/v3 from 3.15.2 to 3.17.3 (#822) 2025-04-14 13:14:20 +02:00
dependabot[bot]
8ebd6ee997
Bump helm.sh/helm/v3 from 3.15.2 to 3.17.3
Bumps [helm.sh/helm/v3](https://github.com/helm/helm) from 3.15.2 to 3.17.3.
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.15.2...v3.17.3)

---
updated-dependencies:
- dependency-name: helm.sh/helm/v3
  dependency-version: 3.17.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 11:03:32 +00:00
Philip Laine
1c6a4fd222
Remove use of httputil reverse proxy (#725) 2025-04-10 14:26:23 +02:00
Philip Laine
33d7ee18a8
Remove use of httputil reverse proxy
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-04-10 14:17:40 +02:00
Philip Laine
901cdc32f3
Bump github.com/prometheus/client_golang from 1.21.1 to 1.22.0 (#818) 2025-04-10 13:41:45 +02:00
dependabot[bot]
dba9d6bbfa
Bump github.com/prometheus/client_golang from 1.21.1 to 1.22.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.1 to 1.22.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.21.1...v1.22.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-10 10:52:01 +00:00
Philip Laine
733f2e37ef
Bump github.com/pelletier/go-toml/v2 from 2.2.3 to 2.2.4 (#819) 2025-04-10 12:50:47 +02:00
dependabot[bot]
275fe5dfcb
Bump github.com/pelletier/go-toml/v2 from 2.2.3 to 2.2.4
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.3 to 2.2.4.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.3...v2.2.4)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-version: 2.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 11:37:16 +00:00
Philip Laine
bee83fe95e
Bump google.golang.org/grpc from 1.71.0 to 1.71.1 (#815) 2025-04-07 17:59:35 +02:00
dependabot[bot]
ea9ff620f8
Bump google.golang.org/grpc from 1.71.0 to 1.71.1
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.71.0 to 1.71.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.71.0...v1.71.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 15:53:52 +00:00
Philip Laine
fa2c3a85d6
Bump golang.org/x/sync from 0.12.0 to 0.13.0 (#817) 2025-04-07 17:52:38 +02:00
dependabot[bot]
3fd2d1541f
Bump golang.org/x/sync from 0.12.0 to 0.13.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.12.0 to 0.13.0.
- [Commits](https://github.com/golang/sync/compare/v0.12.0...v0.13.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 15:48:35 +00:00
Philip Laine
3a3a3b426a
Bump github.com/libp2p/go-libp2p-kad-dht from 0.30.2 to 0.31.0 (#816) 2025-04-07 17:47:14 +02:00
dependabot[bot]
c10002ef34
Bump github.com/libp2p/go-libp2p-kad-dht from 0.30.2 to 0.31.0
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.30.2 to 0.31.0.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.30.2...v0.31.0)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-version: 0.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 11:28:22 +00:00
Philip Laine
c7a273a479
Upgrade to Go 1.24.1 and switch to use go tool for helm docs (#812) 2025-03-29 11:33:36 +01:00
Philip Laine
98c4a12a15
Upgrade to Go 1.24.1 and switch to use go tool for helm docs
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-28 18:00:16 +01:00
Philip Laine
945e202cf0
Prepare for v0.1.1 release (#811) 2025-03-28 16:01:57 +01:00
Philip Laine
57c7412dac
Prepare for v0.1.1 release 2025-03-28 15:49:55 +01:00
Philip Laine
70e50e2918
Fix verification of Containerd configuration with suffixes (#806) 2025-03-27 11:47:54 +01:00
Philip Laine
23c8b76cdd
Fix verification of Containerd configuration with suffixes
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-27 11:41:25 +01:00
Philip Laine
3de348c349
Increase timeout to avoid flakiness in conformance tests (#810) 2025-03-27 11:40:51 +01:00
Philip Laine
e2673c5ef9
Increase timeout to avoid flakiness in conformance tests
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-27 11:36:36 +01:00
Philip Laine
17ba8b3d28
Update golangci lint and fix new issues (#807) 2025-03-27 11:22:28 +01:00
Philip Laine
c59a778205
Update golangci lint and fix new issues
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-26 10:05:20 +01:00
Philip Laine
2b6bd45288
Prepare for v0.1.0 release (#802) 2025-03-22 14:20:37 +01:00
Philip Laine
cbf0f8366c
Prepare for v0.1.0 release 2025-03-22 11:13:52 +01:00
Philip Laine
fd425321ce
Fix helm values naming for additionalMirrorTargets and mirroredRegistries. (#801) 2025-03-22 10:23:18 +01:00
Philip Laine
7f435f6997
Fix helm values naming
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-22 10:17:17 +01:00
Philip Laine
a921788407
Fix so that host is closed even when a bootstrap error occurs (#800) 2025-03-22 10:05:25 +01:00
Philip Laine
a0c9f3abcc
Fix so that host is closed even when a bootstrap error occurs
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-22 09:50:00 +01:00
Philip Laine
38d8cf01ad
Bump github.com/prometheus/common from 0.62.0 to 0.63.0 (#795) 2025-03-22 09:31:45 +01:00
dependabot[bot]
5249c40475
Bump github.com/prometheus/common from 0.62.0 to 0.63.0
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.62.0 to 0.63.0.
- [Release notes](https://github.com/prometheus/common/releases)
- [Changelog](https://github.com/prometheus/common/blob/main/RELEASE.md)
- [Commits](https://github.com/prometheus/common/compare/v0.62.0...v0.63.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/common
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-22 08:26:27 +00:00
Philip Laine
26b4964ca6
Remove Kubernetes bootstrapper (#799) 2025-03-22 09:25:20 +01:00
Philip Laine
e267f04f17
Remove Kubernetes bootstrapper
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-22 09:21:26 +01:00
Philip Laine
6a39e83da5
Restart Spegel if Containerd event subscription is disconnected (#798) 2025-03-21 23:35:27 +01:00
Philip Laine
f8cd9c20f4
Restart Spegel if Containerd event subscription is disconnected
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-21 23:15:11 +01:00
Philip Laine
c3f84ad3a6
Bump golang.org/x/net from 0.35.0 to 0.36.0 (#797) 2025-03-21 13:49:02 +01:00
dependabot[bot]
36cf8ef043
Bump golang.org/x/net from 0.35.0 to 0.36.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 12:41:35 +00:00
Philip Laine
17a2eb972c
Remove name from OCI image struct (#796) 2025-03-21 13:26:46 +01:00
Philip Laine
ec29896b9d
Remove name from OCI image struct
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-21 13:21:46 +01:00
Philip Laine
01c5396a95
Set default memory request and limit in Helm chart (#794) 2025-03-21 13:09:02 +01:00
Philip Laine
219473467e
Set default memory request and limit in Helm chart
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-21 13:03:51 +01:00
Philip Laine
bce49420b6
Add debug view to help validating Spegel (#791) 2025-03-21 10:28:47 +01:00
Philip Laine
34ac563e39
Add debug view to help validating Spegel
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-20 12:43:07 +01:00
Philip Laine
29e4a51739
Bump github.com/spf13/afero from 1.12.0 to 1.14.0 (#786) 2025-03-20 10:41:16 +01:00
dependabot[bot]
f726fb16ef
Bump github.com/spf13/afero from 1.12.0 to 1.14.0
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.12.0 to 1.14.0.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.12.0...v1.14.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/afero
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-20 08:47:13 +00:00
Philip Laine
682b00ae2f
Bump github.com/containerd/containerd/v2 from 2.0.3 to 2.0.4 (#789) 2025-03-20 09:45:59 +01:00
dependabot[bot]
616b984747
Bump github.com/containerd/containerd/v2 from 2.0.3 to 2.0.4
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.0.3 to 2.0.4.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-20 08:21:02 +00:00
Philip Laine
dc964cd3ef
Bump golangci/golangci-lint-action from 6.5.1 to 6.5.2 (#788) 2025-03-19 21:20:42 +01:00
dependabot[bot]
a4d14c03da
Bump golangci/golangci-lint-action from 6.5.1 to 6.5.2
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.5.1 to 6.5.2.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](4696ba8bab...55c2c1448f)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-19 20:04:44 +00:00
Philip Laine
c2beb85269
Bump actions/setup-go from 5.3.0 to 5.4.0 (#790) 2025-03-19 21:03:50 +01:00
dependabot[bot]
ed27c84784
Bump actions/setup-go from 5.3.0 to 5.4.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.3.0 to 5.4.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](f111f3307d...0aaccfd150)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-19 19:58:34 +00:00
Philip Laine
d83001abd6
Add dev deploy recipe to simplify local development (#792) 2025-03-19 20:56:38 +01:00
Philip Laine
ee3997f3d5
Add dev deploy recipe to simplify local development
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-19 20:50:01 +01:00
Philip Laine
c6a1f03564
Refactor OCI image to allow parsing without digest (#787) 2025-03-17 18:28:43 +01:00
Philip Laine
2ff095218d
Refactor OCI image to allow parsing without digest
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-17 18:18:02 +01:00
Philip Laine
fcaace4836
Fix verification of digests when parsing distribution path (#785) 2025-03-17 11:52:28 +01:00
Philip Laine
1ba3c975b3
Fix verification of digests when parsing distribution path
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-17 11:40:37 +01:00
Philip Laine
f9545eeccf
Refactor distribution and move to OCI package (#784) 2025-03-17 11:00:46 +01:00
Philip Laine
2f8e8b254a
Refactor distribution and move to OCI package
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-17 10:53:58 +01:00
Philip Laine
2a96ab7f84
Bump golangci/golangci-lint-action from 6.5.0 to 6.5.1 (#782) 2025-03-15 20:05:15 +01:00
dependabot[bot]
48372bab9e
Bump golangci/golangci-lint-action from 6.5.0 to 6.5.1
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.5.0 to 6.5.1.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](2226d7cb06...4696ba8bab)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-15 19:00:15 +00:00
Philip Laine
68caeebb94
Bump the k8s group with 2 updates (#781) 2025-03-15 19:59:18 +01:00
dependabot[bot]
8077af380e
Bump the k8s group with 2 updates
Bumps the k8s group with 2 updates: [k8s.io/client-go](https://github.com/kubernetes/client-go) and [k8s.io/cri-api](https://github.com/kubernetes/cri-api).


Updates `k8s.io/client-go` from 0.32.2 to 0.32.3
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.32.2...v0.32.3)

Updates `k8s.io/cri-api` from 0.32.2 to 0.32.3
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.32.2...v0.32.3)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
- dependency-name: k8s.io/cri-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-15 18:25:43 +00:00
Philip Laine
8434e6c648
Bump docker/login-action from 3.3.0 to 3.4.0 (#783) 2025-03-15 19:14:37 +01:00
dependabot[bot]
079790c7f9
Bump docker/login-action from 3.3.0 to 3.4.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](9780b0c442...74a5d14239)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-14 12:01:09 +00:00
Philip Laine
7ea98b1ed3
Replace interface{} with any alias (#778) 2025-03-07 11:06:48 +01:00
Philip Laine
88f2eb77bd
Replace interface{} with any alias
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-07 11:02:01 +01:00
Philip Laine
435da8e8e9
Add basic authentication support (#771) 2025-03-06 21:34:26 +01:00
Philip Laine
8b15d80a3c
Add basic authentication support
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-03-06 19:54:14 +01:00
Philip Laine
f4fcaf8379
Fix flake in p2p test caused by race condition (#777) 2025-03-06 19:53:58 +01:00
Philip Laine
9462a3822d
Fix flake in p2p test caused by race condition 2025-03-06 19:45:23 +01:00
Philip Laine
ef8809edbf
fix(metrics): use appropriate buckets for response size (#762) 2025-03-06 19:35:05 +01:00
Josef Kolář
d91ce71799 fix(metrics): use appropriate buckets for response size 2025-03-06 18:45:36 +01:00
Philip Laine
800181ad80
Bump google.golang.org/grpc from 1.70.0 to 1.71.0 (#774) 2025-03-06 18:44:01 +01:00
dependabot[bot]
5a99a5d76a
Bump google.golang.org/grpc from 1.70.0 to 1.71.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.70.0 to 1.71.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.70.0...v1.71.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 17:39:13 +00:00
Philip Laine
52cc89ef26
Increase timeout waiting for conformance tests (#776) 2025-03-06 18:36:20 +01:00
Philip Laine
952046a658
Increase timeout waiting for conformance tests 2025-03-06 15:54:58 +01:00
Philip Laine
d3be7ed058
Bump golang.org/x/sync from 0.11.0 to 0.12.0 (#775) 2025-03-06 14:56:34 +01:00
dependabot[bot]
75f407f585
Bump golang.org/x/sync from 0.11.0 to 0.12.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.11.0 to 0.12.0.
- [Commits](https://github.com/golang/sync/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 11:37:50 +00:00
Philip Laine
fa02fdaf50
Bump github.com/prometheus/client_golang from 1.21.0 to 1.21.1 (#772) 2025-03-06 12:21:19 +01:00
dependabot[bot]
fdb26e38fe
Bump github.com/prometheus/client_golang from 1.21.0 to 1.21.1
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.0 to 1.21.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.21.0...v1.21.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 08:48:20 +00:00
Philip Laine
41d739349d
Bump github.com/libp2p/go-libp2p-kad-dht from 0.30.0 to 0.30.2 (#773) 2025-03-06 09:47:03 +01:00
dependabot[bot]
8ed5021581
Bump github.com/libp2p/go-libp2p-kad-dht from 0.30.0 to 0.30.2
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.30.0 to 0.30.2.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.30.0...v0.30.2)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 12:01:22 +00:00
Philip Laine
386b48365d
Bump docker/setup-qemu-action from 3.5.0 to 3.6.0 (#764) 2025-03-04 20:17:29 +01:00
dependabot[bot]
59e61b9ffc
Bump docker/setup-qemu-action from 3.5.0 to 3.6.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](5964de0df5...29109295f8)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 19:12:16 +00:00
Philip Laine
e2b98b2f4c
Bump github.com/containerd/containerd/v2 from 2.0.2 to 2.0.3 (#763) 2025-03-04 20:11:22 +01:00
dependabot[bot]
3579290824
Bump github.com/containerd/containerd/v2 from 2.0.2 to 2.0.3
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.0.2 to 2.0.3.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v2.0.2...v2.0.3)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 17:54:36 +00:00
Philip Laine
275c64f72c
Bump github.com/libp2p/go-libp2p-kad-dht from 0.29.2 to 0.30.0 (#770) 2025-03-04 18:53:16 +01:00
dependabot[bot]
173ad2736b
Bump github.com/libp2p/go-libp2p-kad-dht from 0.29.2 to 0.30.0
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.29.2 to 0.30.0.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.29.2...v0.30.0)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 17:43:26 +00:00
Philip Laine
7c3a1273df
Bump github.com/opencontainers/image-spec from 1.1.0 to 1.1.1 (#769) 2025-03-04 18:42:04 +01:00
dependabot[bot]
691a90e2a9
Bump github.com/opencontainers/image-spec from 1.1.0 to 1.1.1
Bumps [github.com/opencontainers/image-spec](https://github.com/opencontainers/image-spec) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/opencontainers/image-spec/releases)
- [Changelog](https://github.com/opencontainers/image-spec/blob/main/RELEASES.md)
- [Commits](https://github.com/opencontainers/image-spec/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: github.com/opencontainers/image-spec
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 11:11:33 +00:00
Philip Laine
13ba8976be
Bump docker/setup-qemu-action from 3.4.0 to 3.5.0 (#758) 2025-02-28 12:43:59 +01:00
dependabot[bot]
2c5e53ad41
Bump docker/setup-qemu-action from 3.4.0 to 3.5.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](4574d27a47...5964de0df5)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-28 11:39:17 +00:00
Philip Laine
50c4518659
Bump docker/build-push-action from 6.14.0 to 6.15.0 (#756) 2025-02-28 12:38:29 +01:00
dependabot[bot]
732be3f208
Bump docker/build-push-action from 6.14.0 to 6.15.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.14.0 to 6.15.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](0adf995921...471d1dc4e0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-28 11:30:15 +00:00
Philip Laine
cfbf7e0eff
Bump docker/setup-buildx-action from 3.9.0 to 3.10.0 (#757) 2025-02-28 12:29:22 +01:00
dependabot[bot]
ba6e046e71
Bump docker/setup-buildx-action from 3.9.0 to 3.10.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](f7ce87c1d6...b5ca514318)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-28 09:50:36 +00:00
Philip Laine
781e94e63c
Bump codecov/codecov-action from 5.3.1 to 5.4.0 (#759) 2025-02-28 10:49:41 +01:00
dependabot[bot]
11ee8d1932
Bump codecov/codecov-action from 5.3.1 to 5.4.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.3.1 to 5.4.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](13ce06bfc6...0565863a31)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-27 16:48:39 +00:00
Philip Laine
e2889e6766
Bump docker/metadata-action from 5.6.1 to 5.7.0 (#760) 2025-02-27 17:47:36 +01:00
dependabot[bot]
01390bd275
Bump docker/metadata-action from 5.6.1 to 5.7.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.6.1 to 5.7.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](369eb591f4...902fa8ec7d)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-27 12:09:32 +00:00
Philip Laine
946df8a278
Bump github.com/libp2p/go-libp2p-kad-dht from 0.29.1 to 0.29.2 (#761) 2025-02-27 13:08:22 +01:00
dependabot[bot]
cf7128a5b0
Bump github.com/libp2p/go-libp2p-kad-dht from 0.29.1 to 0.29.2
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.29.1 to 0.29.2.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.29.1...v0.29.2)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-27 11:24:59 +00:00
Philip Laine
83849f056c
Add note for community meetings (#754) 2025-02-26 12:39:43 +01:00
Philip Laine
8d1ce6aa30
Add note for community meetings 2025-02-26 11:26:51 +01:00
Philip Laine
73efba28d6
Set GOMAXPROCS and GOMEMLIMIT when limits are set (#753) 2025-02-26 11:13:51 +01:00
Philip Laine
36369eedbe
Set GOMAXPROCS and GOMEMLIMIT when limits are set
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-02-26 11:07:11 +01:00
Philip Laine
e1162ef751
Implement support to disable registry filtering in Containerd (#373) 2025-02-26 00:12:42 +01:00
Philip Laine
f5106777c5
Implement support to disable registry filtering
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-02-25 23:20:33 +01:00
Philip Laine
af3d364e86
Rename append mirrors to prepend existing (#750) 2025-02-25 13:40:16 +01:00
Philip Laine
4fb7505f03
Rename append mirrors to prepend existing
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-02-25 13:10:07 +01:00
Philip Laine
757570c9c7
Fix topology annotation (#748) 2025-02-25 12:26:31 +01:00
Philip Laine
ea517a0642
Fix topology annotation
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-02-25 12:21:54 +01:00
Philip Laine
f3778ebfdd
Update Go to 1.23.6 (#747) 2025-02-25 11:56:05 +01:00
Philip Laine
80be70dfc6
Update Go to 1.23.6
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-02-25 11:50:31 +01:00
Philip Laine
ea22d86868
Update conformance version (#745) 2025-02-25 11:14:15 +01:00
Philip Laine
c283369337
Update conformance version
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-02-25 11:09:34 +01:00
Philip Laine
3637460b1b
Bump github.com/libp2p/go-libp2p-kad-dht from 0.29.0 to 0.29.1 (#746) 2025-02-24 14:40:04 +01:00
dependabot[bot]
e293063b42
Bump github.com/libp2p/go-libp2p-kad-dht from 0.29.0 to 0.29.1
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.29.0 to 0.29.1.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.29.0...v0.29.1)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 11:51:58 +00:00
Philip Laine
3484839e9c
Bump sigstore/cosign-installer from 3.8.0 to 3.8.1 (#742) 2025-02-24 10:42:21 +01:00
dependabot[bot]
c69700375d
Bump sigstore/cosign-installer from 3.8.0 to 3.8.1
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.8.0 to 3.8.1.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](c56c2d3e59...d7d6bc7722)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 09:36:20 +00:00
Philip Laine
ac56972cdd
Bump github.com/prometheus/client_golang from 1.20.5 to 1.21.0 (#740) 2025-02-24 10:35:05 +01:00
dependabot[bot]
b8dcdb7d01
Bump github.com/prometheus/client_golang from 1.20.5 to 1.21.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.5 to 1.21.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.21.0/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.5...v1.21.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 09:30:00 +00:00
Philip Laine
556f35dd88
fix(charts): remove metrics label from bootstrap svc (#743) 2025-02-24 10:28:18 +01:00
Josef Kolář
4acc0861a6 fix(charts): removed metrics component label from bootstrap svc 2025-02-24 10:11:34 +01:00
Philip Laine
42201b5760
Move adopters list to website (#744) 2025-02-23 12:49:07 +01:00
Philip Laine
18d0dbe805
Move adopters list to website 2025-02-23 12:42:23 +01:00
Philip Laine
830beb808e
Bump docker/build-push-action from 6.13.0 to 6.14.0 (#741) 2025-02-21 00:19:23 +01:00
dependabot[bot]
3f53e9fd77
Bump docker/build-push-action from 6.13.0 to 6.14.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.13.0 to 6.14.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](ca877d9245...0adf995921)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-20 11:33:52 +00:00
Philip Laine
15e2144b92
Bump the k8s group with 2 updates (#734) 2025-02-18 13:56:52 +01:00
dependabot[bot]
fa4ef1e859
Bump the k8s group with 2 updates
Bumps the k8s group with 2 updates: [k8s.io/client-go](https://github.com/kubernetes/client-go) and [k8s.io/cri-api](https://github.com/kubernetes/cri-api).


Updates `k8s.io/client-go` from 0.32.1 to 0.32.2
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.32.1...v0.32.2)

Updates `k8s.io/cri-api` from 0.32.1 to 0.32.2
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.32.1...v0.32.2)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
- dependency-name: k8s.io/cri-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-18 12:51:02 +00:00
Philip Laine
3c77598a7a
Bump azure/setup-helm from 4.2.0 to 4.3.0 (#738) 2025-02-18 13:49:37 +01:00
dependabot[bot]
3f76df52e9
Bump azure/setup-helm from 4.2.0 to 4.3.0
Bumps [azure/setup-helm](https://github.com/azure/setup-helm) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/azure/setup-helm/releases)
- [Changelog](https://github.com/Azure/setup-helm/blob/main/CHANGELOG.md)
- [Commits](fe7b79cd5e...b9e51907a0)

---
updated-dependencies:
- dependency-name: azure/setup-helm
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-18 12:15:31 +00:00
Philip Laine
b26f21674a
Fix e2e tests for Kind v0.27.0 (#737) 2025-02-18 13:14:33 +01:00
Philip Laine
b1344c563c
Fix e2e tests for Kind v0.27.0
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-02-18 13:09:55 +01:00
Philip Laine
d465325195
Bump golangci/golangci-lint-action from 6.3.0 to 6.5.0 (#736) 2025-02-17 15:48:14 +01:00
dependabot[bot]
5bd8931581
Bump golangci/golangci-lint-action from 6.3.0 to 6.5.0
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.3.0 to 6.5.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](e60da84bfa...2226d7cb06)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 12:38:49 +00:00
Philip Laine
b6a8e208f0
Bump docker/setup-qemu-action from 3.3.0 to 3.4.0 (#728) 2025-02-07 22:20:42 +01:00
dependabot[bot]
ec18a3bbdf
Bump docker/setup-qemu-action from 3.3.0 to 3.4.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](53851d1459...4574d27a47)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-07 20:07:55 +00:00
Philip Laine
35ff4bb2da
Bump docker/setup-buildx-action from 3.8.0 to 3.9.0 (#727) 2025-02-07 21:06:57 +01:00
dependabot[bot]
554066c290
Bump docker/setup-buildx-action from 3.8.0 to 3.9.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.8.0 to 3.9.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](6524bf65af...f7ce87c1d6)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-07 11:19:58 +00:00
Philip Laine
765c26ad3d
Bump golang.org/x/sync from 0.10.0 to 0.11.0 (#721) 2025-02-06 19:12:59 +01:00
dependabot[bot]
fa0be0a9b5
Bump golang.org/x/sync from 0.10.0 to 0.11.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.10.0 to 0.11.0.
- [Commits](https://github.com/golang/sync/compare/v0.10.0...v0.11.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-06 18:06:44 +00:00
Philip Laine
42433fd463
Bump golangci/golangci-lint-action from 6.2.0 to 6.3.0 (#723) 2025-02-06 19:05:20 +01:00
dependabot[bot]
e244c0087e
Bump golangci/golangci-lint-action from 6.2.0 to 6.3.0
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](ec5d18412c...e60da84bfa)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-06 18:00:39 +00:00
Philip Laine
dd4d93ee33
Bump go.etcd.io/bbolt from 1.3.11 to 1.4.0 (#726) 2025-02-06 18:59:31 +01:00
dependabot[bot]
62791e25eb
Bump go.etcd.io/bbolt from 1.3.11 to 1.4.0
Bumps [go.etcd.io/bbolt](https://github.com/etcd-io/bbolt) from 1.3.11 to 1.4.0.
- [Release notes](https://github.com/etcd-io/bbolt/releases)
- [Commits](https://github.com/etcd-io/bbolt/compare/v1.3.11...v1.4.0)

---
updated-dependencies:
- dependency-name: go.etcd.io/bbolt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-06 17:53:44 +00:00
Philip Laine
365ed16467
Bump sigstore/cosign-installer from 3.7.0 to 3.8.0 (#722) 2025-02-06 18:51:58 +01:00
dependabot[bot]
4e023b5822
Bump sigstore/cosign-installer from 3.7.0 to 3.8.0
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.7.0 to 3.8.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](dc72c7d5c4...c56c2d3e59)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-06 17:46:35 +00:00
Philip Laine
cd8ddbef75
Update conformance test version in e2e tests (#724) 2025-02-05 20:22:49 +01:00
Philip Laine
657358c7ed
Update conformance test version in e2e tests 2025-02-05 20:14:40 +01:00
Philip Laine
eaf6e8a670
Bump github.com/libp2p/go-libp2p-kad-dht from 0.28.2 to 0.29.0 (#719) 2025-02-03 16:42:52 +01:00
dependabot[bot]
2db6708bc6
Bump github.com/libp2p/go-libp2p-kad-dht from 0.28.2 to 0.29.0
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.28.2 to 0.29.0.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.28.2...v0.29.0)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-30 11:24:59 +00:00
Philip Laine
3d7796069a
Extend tests for distribution (#717) 2025-01-28 15:47:56 +01:00
Philip Laine
d9f9d545d5
Extend tests for distribution
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-28 13:04:34 +01:00
Philip Laine
5305285c9f
Pin action versions with commit hashes (#716) 2025-01-27 10:56:20 +01:00
Philip Laine
53553948e3
Pin action versions with commit hashes 2025-01-27 10:48:45 +01:00
Philip Laine
4cb59e326c
Remove v prefix from released Helm chart version (#714) 2025-01-24 15:56:50 +01:00
Philip Laine
40d99822fc
Remove v prefix from released Helm chart version 2025-01-24 15:50:02 +01:00
Philip Laine
3db4a1094b
Bump google.golang.org/grpc from 1.69.4 to 1.70.0 (#713) 2025-01-24 14:30:09 +01:00
dependabot[bot]
3d542cd48f
Bump google.golang.org/grpc from 1.69.4 to 1.70.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.69.4 to 1.70.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.69.4...v1.70.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-24 11:54:29 +00:00
Philip Laine
f20a91eb5f
Update Libp2p v0.38.2 (#712) 2025-01-23 11:00:34 +01:00
Philip Laine
1a2b547394
Update Libp2p v0.38.2
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-22 17:05:30 +01:00
Philip Laine
58347851ef
Bump google.golang.org/grpc from 1.67.3 to 1.69.4 (#711) 2025-01-22 16:56:53 +01:00
dependabot[bot]
d2858efb25
Bump google.golang.org/grpc from 1.67.3 to 1.69.4
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.67.3 to 1.69.4.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.67.3...v1.69.4)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 15:52:07 +00:00
Philip Laine
59435f714f
Bump github.com/ipfs/go-cid from 0.4.1 to 0.5.0 (#710) 2025-01-22 14:24:17 +01:00
dependabot[bot]
1c31048a30
Bump github.com/ipfs/go-cid from 0.4.1 to 0.5.0
Bumps [github.com/ipfs/go-cid](https://github.com/ipfs/go-cid) from 0.4.1 to 0.5.0.
- [Release notes](https://github.com/ipfs/go-cid/releases)
- [Commits](https://github.com/ipfs/go-cid/compare/v0.4.1...v0.5.0)

---
updated-dependencies:
- dependency-name: github.com/ipfs/go-cid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 11:32:19 +00:00
Philip Laine
6092d081bf
Prepare v0.0.30 (#705) 2025-01-17 19:53:33 +01:00
Philip Laine
8ea7ad42e3
Prepare v0.0.30 2025-01-17 19:45:14 +01:00
Philip Laine
0b796a9eba
Update Containerd client to v2 (#704) 2025-01-17 18:00:18 +01:00
Philip Laine
8845a4127f
Update Containerd client to v2
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-17 17:53:36 +01:00
Philip Laine
4adb74668b
Fix p2p router close panic and add tests (#703) 2025-01-17 14:36:01 +01:00
Philip Laine
57db00f66f
Fix p2p router close panic and add tests
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-17 14:29:20 +01:00
Philip Laine
3c85d7e358
Refactor and add tests for p2p ready (#702) 2025-01-17 13:52:04 +01:00
Philip Laine
56c70cc05c
Refactor and add tests for p2p ready
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-17 13:10:55 +01:00
Philip Laine
8d7953a0f9
Bump the k8s group with 2 updates (#700) 2025-01-16 17:02:15 +01:00
dependabot[bot]
510e22d5f0
Bump the k8s group with 2 updates
Bumps the k8s group with 2 updates: [k8s.io/client-go](https://github.com/kubernetes/client-go) and [k8s.io/cri-api](https://github.com/kubernetes/cri-api).


Updates `k8s.io/client-go` from 0.32.0 to 0.32.1
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.32.0...v0.32.1)

Updates `k8s.io/cri-api` from 0.32.0 to 0.32.1
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.32.0...v0.32.1)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
- dependency-name: k8s.io/cri-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-16 15:55:18 +00:00
Philip Laine
6732c3c1c9
Default to using bash in all GitHub workflows (#698) 2025-01-16 16:42:48 +01:00
Philip Laine
66e0b55bb6
Default to using bash in all GitHub workflows
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-16 16:36:06 +01:00
Philip Laine
8d8f1090ec
Rewrite e2e tests in Go (#701) 2025-01-16 16:35:14 +01:00
Philip Laine
502aca0e72
Rewrite e2e tests in Go
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-16 16:29:29 +01:00
Philip Laine
d56038cd42
Remove as mismatch error and replace with errors as (#699) 2025-01-15 21:14:25 +01:00
Philip Laine
706a39e13d
Remove as missmatch error and replace with errors as
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-15 21:07:02 +01:00
Philip Laine
512cd2ded8
add commonLabels for pods (#693) 2025-01-15 20:20:35 +01:00
Juri Malinovski
2e61024e58 Fix typo
Signed-off-by: Juri Malinovski <juri.malinovski@coolbet.com>
2025-01-15 13:51:53 +02:00
Juri Malinovski
4eabfe99d5 Update CHANGELOG.md
Signed-off-by: Juri Malinovski <juri.malinovski@coolbet.com>
2025-01-15 13:50:16 +02:00
Juri Malinovski
817c56f42a
Merge branch 'main' into main 2025-01-15 13:01:15 +02:00
Philip Laine
db1da7bae7
Fix DNS bootstrap self check (#696) 2025-01-15 11:48:19 +01:00
Philip Laine
0240a6216b
Fix DNS bootstrap self check
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-15 11:24:20 +01:00
Philip Laine
64f571f02b
Replace IP in multi address with manet (#694) 2025-01-14 12:35:20 +01:00
Philip Laine
4b7f7412d7
Replace IP in multi address with manet
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-14 12:26:30 +01:00
Juri Malinovski
55632310dc add commonLabels for pods
Signed-off-by: Juri Malinovski <juri.malinovski@coolbet.com>
2025-01-13 14:49:47 +02:00
Philip Laine
a4654f6046
Bump github.com/containerd/containerd from 1.7.24 to 1.7.25 (#690) 2025-01-13 12:07:11 +01:00
dependabot[bot]
33e6c02cb3
Bump github.com/containerd/containerd from 1.7.24 to 1.7.25
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.24 to 1.7.25.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.24...v1.7.25)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 10:59:00 +00:00
Philip Laine
c3e3eacb62
Bump github.com/spf13/afero from 1.11.0 to 1.12.0 (#691) 2025-01-13 11:57:41 +01:00
dependabot[bot]
09d4d9e12e
Bump github.com/spf13/afero from 1.11.0 to 1.12.0
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/afero
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 10:46:48 +00:00
Philip Laine
318c87279b
Bump github.com/libp2p/go-libp2p-kad-dht from 0.28.1 to 0.28.2 (#692) 2025-01-13 11:45:28 +01:00
dependabot[bot]
8c9abc35f7
Bump github.com/libp2p/go-libp2p-kad-dht from 0.28.1 to 0.28.2
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.28.1 to 0.28.2.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.28.1...v0.28.2)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 10:40:48 +00:00
Philip Laine
7daa541141
Make cluster domain configurable (#689) 2025-01-13 10:32:10 +01:00
Philip Laine
0c5a6cfcff
Make cluster domain configurable
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-13 10:25:48 +01:00
Philip Laine
46b72c234e
Prepare v0.0.29 (#685) 2025-01-10 21:21:29 +01:00
Philip Laine
40b4d7bf71
Prepare v0.0.29 2025-01-10 21:15:54 +01:00
Philip Laine
7b07a5b863
Switch to using headless service for bootstrapping (#680) 2025-01-10 20:46:28 +01:00
Philip Laine
484856cfc1
Switch to using headless service for bootstrapping
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-10 20:35:46 +01:00
Philip Laine
a0a53617e4
Allow bootstrappers to return multiaddress only containing IP (#684) 2025-01-10 15:56:00 +01:00
Philip Laine
09b99613ee
Allow bootstrappers to return multiaddress only containing IP
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-10 15:50:36 +01:00
Philip Laine
cf851a4065
Change bootstrapper to allow returning multiple peers (#683) 2025-01-09 13:16:45 +01:00
Philip Laine
4dfee118c6
Change bootstrapper to allow returning multiple peers
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-09 13:11:40 +01:00
Philip Laine
0330584571
Add importas linter (#681) 2025-01-09 12:45:09 +01:00
Philip Laine
44c45bdefe
Add importas linter
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-09 12:36:04 +01:00
Philip Laine
d78dd142f8
feat(helm): add commonLabels support (#678) 2025-01-07 18:02:24 +01:00
Devin Buhl
c5ed3c739d
Update CHANGELOG.md
Co-authored-by: Philip Laine <philip.laine@gmail.com>
2025-01-07 07:09:14 -05:00
Devin Buhl
b4e2523282
fix: run make helm-docs
Signed-off-by: Devin Buhl <devin@buhl.casa>
2025-01-06 19:23:57 -05:00
Devin Buhl
ff25044835
feat(helm): add commonLabels support
Signed-off-by: Devin Buhl <devin@buhl.casa>
2025-01-06 19:13:48 -05:00
Philip Laine
bc4b11a8c0
Update github.com/libp2p/go-libp2p to v0.38.1 (#676) 2025-01-06 14:43:37 +01:00
Philip Laine
a75debc3a7
Update github.com/libp2p/go-libp2p to v0.38.1
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2025-01-06 14:30:42 +01:00
Philip Laine
4bedc78120
Update golang.org/x/net to v0.33.0 (#675) 2025-01-06 14:17:22 +01:00
Philip Laine
91de1d215f
Update golang.org/x/net to v0.33.0 2025-01-06 14:11:23 +01:00
Philip Laine
6102823f93
Migrate documentation to use website (#674) 2025-01-01 20:29:04 +01:00
Philip Laine
17df2bbc7b
Migrate documentation to use website 2025-01-01 20:23:58 +01:00
Philip Laine
55e13042bf
Prepare v0.0.28 (#668) 2024-12-16 22:31:42 +01:00
Philip Laine
12f7c7a1f9
Prepare v0.0.28 2024-12-16 21:59:46 +01:00
Philip Laine
04d72501a4
Bump golang.org/x/crypto from 0.28.0 to 0.31.0 (#667) 2024-12-16 21:47:26 +01:00
dependabot[bot]
6f0cbd9aef
Bump golang.org/x/crypto from 0.28.0 to 0.31.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.28.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.28.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 20:39:10 +00:00
Philip Laine
a5ea612dd9
Bump the k8s group with 2 updates (#665) 2024-12-16 21:28:36 +01:00
dependabot[bot]
7c7654e97d
Bump the k8s group with 2 updates
Bumps the k8s group with 2 updates: [k8s.io/client-go](https://github.com/kubernetes/client-go) and [k8s.io/cri-api](https://github.com/kubernetes/cri-api).


Updates `k8s.io/client-go` from 0.31.4 to 0.32.0
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.31.4...v0.32.0)

Updates `k8s.io/cri-api` from 0.31.4 to 0.32.0
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.31.4...v0.32.0)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: k8s
- dependency-name: k8s.io/cri-api
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: k8s
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-12 12:04:43 +00:00
Philip Laine
86ac671750
Bump the k8s group with 2 updates (#664) 2024-12-11 20:20:36 +01:00
dependabot[bot]
c4487526c9
Bump the k8s group with 2 updates
Bumps the k8s group with 2 updates: [k8s.io/client-go](https://github.com/kubernetes/client-go) and [k8s.io/cri-api](https://github.com/kubernetes/cri-api).


Updates `k8s.io/client-go` from 0.31.3 to 0.31.4
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.31.3...v0.31.4)

Updates `k8s.io/cri-api` from 0.31.3 to 0.31.4
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.31.3...v0.31.4)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
- dependency-name: k8s.io/cri-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-11 11:44:54 +00:00
Philip Laine
1e751d29f1
Add allocs to pprof endpoints (#661) 2024-12-06 13:56:14 +01:00
Philip Laine
2b9b709d0b
Add allocs to pprof endpoints
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-12-06 13:46:39 +01:00
Philip Laine
4f09f3ff53
Add accept ranges header to blob HEAD request (#660) 2024-12-06 13:44:29 +01:00
Philip Laine
a03fa8287b
Add accept ranges header to blob HEAD request
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-12-06 13:38:23 +01:00
Philip Laine
b682a7fab7
Bump go-libp2p to address quic-go CVE (#659) 2024-12-06 09:20:01 +01:00
Derek Nola
bec7c278b5
Bump go-libp2p to address quic-go CVE
Signed-off-by: Derek Nola <derek.nola@suse.com>
2024-12-05 11:57:44 -08:00
Philip Laine
78dcae8651
Bump golang.org/x/sync from 0.9.0 to 0.10.0 (#657) 2024-12-05 18:37:13 +01:00
dependabot[bot]
65841a53df
Bump golang.org/x/sync from 0.9.0 to 0.10.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.9.0 to 0.10.0.
- [Commits](https://github.com/golang/sync/compare/v0.9.0...v0.10.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-05 11:31:07 +00:00
Philip Laine
0801ee9872
Update ADOPTERS.md to add Nemlig. (#655) 2024-12-02 12:49:03 +01:00
Lars Bing Bong.
3d41ba350d
Update ADOPTERS.md to add Nemlig.
You - @phillebaba - contacted me on LinkedIn to ask me about whether this would be okay. Totally and here we go.
2024-11-30 20:13:15 +01:00
Philip Laine
fa1e6b4db7
Cleanup compatibility docs and add RKE2 to K3S section (#654) 2024-11-28 21:28:02 +01:00
Philip Laine
d8debe47ae
Cleanup compatibility docs and add RKE2 to K3S section
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-11-28 21:15:01 +01:00
Philip Laine
e9ab8a1144
Fix Talos compatibility documentation for discard unpacked layers (#653) 2024-11-28 21:03:35 +01:00
Philip Laine
2302765aed
Fix Talos compatibility documentation for discard unpacked layers
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-11-28 20:58:26 +01:00
Philip Laine
c989989c35
Fix Containerd CRI config verification (#651) 2024-11-28 13:55:28 +01:00
Philip Laine
feb113bea2
Fix Containerd CRI config verification
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-11-28 13:45:51 +01:00
Philip Laine
b1164a3163
Group k8s dependabot dependency updates (#649) 2024-11-26 08:45:47 +01:00
Philip Laine
fe4c0615a2
Group k8s dependabot dependency updates 2024-11-26 08:32:56 +01:00
Philip Laine
6c4e0b28d7
Bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#647) 2024-11-25 16:26:39 +01:00
dependabot[bot]
b641427a84
Bump github.com/stretchr/testify from 1.9.0 to 1.10.0
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 12:00:39 +00:00
Philip Laine
2671da846b
Bump github.com/containerd/containerd from 1.7.23 to 1.7.24 (#645) 2024-11-25 11:02:40 +01:00
dependabot[bot]
9fd897c42c
Bump github.com/containerd/containerd from 1.7.23 to 1.7.24
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.23 to 1.7.24.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.23...v1.7.24)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 09:32:42 +00:00
Philip Laine
e76e489804
Bump github.com/Masterminds/semver/v3 from 3.3.0 to 3.3.1 (#642) 2024-11-25 10:31:03 +01:00
dependabot[bot]
ea8a5e9546
Bump github.com/Masterminds/semver/v3 from 3.3.0 to 3.3.1
Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.3.0 to 3.3.1.
- [Release notes](https://github.com/Masterminds/semver/releases)
- [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Masterminds/semver/compare/v3.3.0...v3.3.1)

---
updated-dependencies:
- dependency-name: github.com/Masterminds/semver/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 09:15:18 +00:00
Philip Laine
5493edcd68
Bump k8s.io/cri-api from 0.31.2 to 0.31.3 (#643) 2024-11-25 10:13:47 +01:00
dependabot[bot]
75656b7241
Bump k8s.io/cri-api from 0.31.2 to 0.31.3
Bumps [k8s.io/cri-api](https://github.com/kubernetes/cri-api) from 0.31.2 to 0.31.3.
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.31.2...v0.31.3)

---
updated-dependencies:
- dependency-name: k8s.io/cri-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 08:49:50 +00:00
Philip Laine
51be071016
Bump k8s.io/client-go from 0.31.2 to 0.31.3 (#644) 2024-11-25 09:48:17 +01:00
dependabot[bot]
503aa41536
Bump k8s.io/client-go from 0.31.2 to 0.31.3
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.31.2 to 0.31.3.
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.31.2...v0.31.3)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 07:51:54 +00:00
Philip Laine
774eb56517
Bump codecov/codecov-action from 4 to 5 (#637) 2024-11-21 14:43:56 +01:00
dependabot[bot]
3068374a4d
Bump codecov/codecov-action from 4 to 5
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-21 12:57:03 +00:00
Philip Laine
5212cd426b
Add RKE2 to adopters list (#641) 2024-11-20 07:50:05 +01:00
Philip Laine
09cfe28b54
Add RKE2 to adopters list
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-11-20 07:43:13 +01:00
Philip Laine
caa446d791
Bump github.com/libp2p/go-libp2p-kad-dht from 0.28.0 to 0.28.1 (#639) 2024-11-19 05:20:34 +01:00
dependabot[bot]
9954a30714
Bump github.com/libp2p/go-libp2p-kad-dht from 0.28.0 to 0.28.1
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.28.0 to 0.28.1.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.28.0...v0.28.1)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-18 11:48:10 +00:00
Philip Laine
cbca8c022c
Add benchmarks for v0.0.27 (#606) 2024-11-15 01:22:10 +01:00
Philip Laine
823c869f5e
Add benchmarks for v0.0.27
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-11-15 01:14:39 +01:00
Philip Laine
c74d9fec97
Bump github.com/libp2p/go-libp2p-kad-dht from 0.27.0 to 0.28.0 (#636) 2024-11-14 20:25:07 +01:00
dependabot[bot]
498a7dd94a
Bump github.com/libp2p/go-libp2p-kad-dht from 0.27.0 to 0.28.0
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.27.0 to 0.28.0.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-14 11:39:59 +00:00
Philip Laine
2145d62b61
Adjust instance variable of Grafana dashboard (#633) 2024-11-12 21:47:18 +01:00
Sebastian Gaiser
b94a96c4d7 fix(charts,spegel,monitoring): adjust instance variable of Grafana dashboard
In order to make the dashboard working again, I fixed the 'instance' variable to use the correct job. In addition all panels got updated and some minor changes for coloring is done.

Signed-off-by: Sebastian Gaiser <sebastiangaiser@users.noreply.github.com>
2024-11-12 19:46:30 +01:00
Philip Laine
edcd015106
Fix Golang CI Lint errors (#635) 2024-11-12 19:46:14 +01:00
Philip Laine
e1ddacd14e
Fix Golang CI Lint errors 2024-11-12 19:04:01 +01:00
Philip Laine
4fb98a6f84
Bump golang.org/x/sync from 0.8.0 to 0.9.0 (#632) 2024-11-08 19:08:04 +01:00
dependabot[bot]
be05498465
Bump golang.org/x/sync from 0.8.0 to 0.9.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.8.0 to 0.9.0.
- [Commits](https://github.com/golang/sync/compare/v0.8.0...v0.9.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-08 11:58:54 +00:00
Philip Laine
bf9475e38c
Bump github.com/containerd/typeurl/v2 from 2.2.2 to 2.2.3 (#631) 2024-11-07 17:20:28 +01:00
dependabot[bot]
cd77ca194c
Bump github.com/containerd/typeurl/v2 from 2.2.2 to 2.2.3
Bumps [github.com/containerd/typeurl/v2](https://github.com/containerd/typeurl) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/containerd/typeurl/releases)
- [Commits](https://github.com/containerd/typeurl/compare/v2.2.2...v2.2.3)

---
updated-dependencies:
- dependency-name: github.com/containerd/typeurl/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-07 11:28:04 +00:00
Philip Laine
2432c11838
Add EKS AL2023/nodeadm configuration documentation (#628) 2024-11-06 01:06:26 +01:00
Philip Laine
f69a9e78c9
Merge branch 'main' into al2023-doc 2024-11-05 23:08:36 +01:00
Philip Laine
aa14f78f50
Document how to use multiple Spegel deployments in the same cluster (#629) 2024-11-05 17:34:43 +01:00
Philip Laine
55b3e14cdc
Document how to use multiple Spegel deployments in the same cluster
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-11-05 17:05:13 +01:00
Tony Dai
529edb4513
Add AL2023/nodeadm configuration documentation 2024-11-05 10:51:54 -05:00
Philip Laine
69d710b79d
Bump github.com/containerd/typeurl/v2 from 2.2.1 to 2.2.2 (#627) 2024-11-05 15:59:45 +01:00
dependabot[bot]
d201a85040
Bump github.com/containerd/typeurl/v2 from 2.2.1 to 2.2.2
Bumps [github.com/containerd/typeurl/v2](https://github.com/containerd/typeurl) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/containerd/typeurl/releases)
- [Commits](https://github.com/containerd/typeurl/compare/v2.2.1...v2.2.2)

---
updated-dependencies:
- dependency-name: github.com/containerd/typeurl/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-05 14:50:30 +00:00
Philip Laine
80eda5b203
feature support range requests for blobs (#576) 2024-11-05 15:48:24 +01:00
Philip Laine
5075a63571
Merge branch 'main' into feature/blobs-http-range 2024-11-05 15:42:28 +01:00
Tim
0b2ee7d7e1 feat(blobs): http range section reader 2024-11-05 14:30:13 +08:00
Philip Laine
e1e62b3069
docs: add technical flow mermaid diagrams (#621) 2024-11-04 18:22:19 +01:00
Philip Laine
52a5fb91a9
Merge branch 'main' into docs/add-diagrams 2024-11-04 17:33:35 +01:00
Philip Laine
99ac49696c
Bump github.com/containerd/errdefs from 0.3.0 to 1.0.0 (#625) 2024-11-04 17:32:23 +01:00
dependabot[bot]
f76451b1c5
Bump github.com/containerd/errdefs from 0.3.0 to 1.0.0
Bumps [github.com/containerd/errdefs](https://github.com/containerd/errdefs) from 0.3.0 to 1.0.0.
- [Release notes](https://github.com/containerd/errdefs/releases)
- [Commits](https://github.com/containerd/errdefs/compare/v0.3.0...v1.0.0)

---
updated-dependencies:
- dependency-name: github.com/containerd/errdefs
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-04 15:55:35 +00:00
Philip Laine
50b0cc0719
Bump github.com/containerd/containerd/api from 1.7.19 to 1.8.0 (#626) 2024-11-04 16:54:10 +01:00
dependabot[bot]
da421e35a1
Bump github.com/containerd/containerd/api from 1.7.19 to 1.8.0
Bumps [github.com/containerd/containerd/api](https://github.com/containerd/containerd) from 1.7.19 to 1.8.0.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.19...api/v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd/api
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-04 11:37:06 +00:00
tremo
f41563d3b7 re-adding fallback but from containerd instead 2024-11-02 18:27:25 +02:00
tremo
f92f50a39d removing fallback desc on first diagram 2024-11-02 18:01:40 +02:00
tremo
c7eb153052 updating cluster arch to remove spegel pod to external reg fallback connections 2024-11-02 18:00:28 +02:00
Philip Laine
697f87f3e2
Fix bootstrap error message spelling (#624) 2024-11-01 16:56:16 +01:00
Philip Laine
321a77a492
Fix bootstrap error message spelling 2024-11-01 16:34:21 +01:00
Philip Laine
80dabcbf9d
Bump github.com/containerd/typeurl/v2 from 2.2.0 to 2.2.1 (#623) 2024-11-01 16:30:01 +01:00
dependabot[bot]
a2746977a4
Bump github.com/containerd/typeurl/v2 from 2.2.0 to 2.2.1
Bumps [github.com/containerd/typeurl/v2](https://github.com/containerd/typeurl) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/containerd/typeurl/releases)
- [Commits](https://github.com/containerd/typeurl/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: github.com/containerd/typeurl/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-01 13:32:08 +00:00
Philip Laine
b15b2b772e
Bump github.com/multiformats/go-multiaddr from 0.13.0 to 0.14.0 (#622) 2024-11-01 14:30:39 +01:00
dependabot[bot]
3b256e3239
Bump github.com/multiformats/go-multiaddr from 0.13.0 to 0.14.0
Bumps [github.com/multiformats/go-multiaddr](https://github.com/multiformats/go-multiaddr) from 0.13.0 to 0.14.0.
- [Release notes](https://github.com/multiformats/go-multiaddr/releases)
- [Commits](https://github.com/multiformats/go-multiaddr/compare/v0.13.0...v0.14.0)

---
updated-dependencies:
- dependency-name: github.com/multiformats/go-multiaddr
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-31 12:01:10 +00:00
Tim
d2761dd6a7 feat(blobs): http range 2024-10-31 17:59:57 +08:00
Tremo
ff5f92431d
Merge branch 'main' into docs/add-diagrams 2024-10-30 18:25:37 +03:00
tremo
758f4271c6 docs: add technical diagrams explaining core components interactions 2024-10-30 18:24:58 +03:00
Philip Laine
30faf12414
Bump k8s.io/cri-api from 0.31.1 to 0.31.2 (#615) 2024-10-25 10:12:08 +02:00
dependabot[bot]
b909e63702
Bump k8s.io/cri-api from 0.31.1 to 0.31.2
Bumps [k8s.io/cri-api](https://github.com/kubernetes/cri-api) from 0.31.1 to 0.31.2.
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.31.1...v0.31.2)

---
updated-dependencies:
- dependency-name: k8s.io/cri-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-24 20:54:37 +00:00
Philip Laine
4f556860d6
Bump k8s.io/client-go from 0.31.1 to 0.31.2 (#616) 2024-10-24 22:53:11 +02:00
dependabot[bot]
e3b040db2e
Bump k8s.io/client-go from 0.31.1 to 0.31.2
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.31.1 to 0.31.2.
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.31.1...v0.31.2)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-24 11:36:57 +00:00
Philip Laine
d296ff8205
Bump github.com/containerd/containerd from 1.7.22 to 1.7.23 (#611) 2024-10-16 13:04:27 +02:00
dependabot[bot]
532a54b1ae
Bump github.com/containerd/containerd from 1.7.22 to 1.7.23
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.22 to 1.7.23.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.22...v1.7.23)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-16 07:42:13 +00:00
Philip Laine
82e76c402d
Bump github.com/prometheus/client_golang from 1.20.4 to 1.20.5 (#610) 2024-10-16 09:40:53 +02:00
dependabot[bot]
aa32a97553
Bump github.com/prometheus/client_golang from 1.20.4 to 1.20.5
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.4 to 1.20.5.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.20.5/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.4...v1.20.5)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-15 11:49:27 +00:00
Philip Laine
a2d0cfeb59
Use custom proxy transport and increase idle connections per host (#608) 2024-10-14 20:29:23 +02:00
Philip Laine
7baad505c3
Use custom proxy transport and increase idle connections per host
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-10-14 18:16:31 +02:00
Philip Laine
9237bce5f3
Prepare v0.0.27 (#605) 2024-10-13 14:49:49 +02:00
Philip Laine
6e4f59920d
Prepare v0.0.27 2024-10-13 14:38:42 +02:00
Philip Laine
8fe5643867
Create empty backup directory when mirror directory is empty (#604) 2024-10-13 14:36:25 +02:00
Philip Laine
cdffff4a10
Create empty backup directory when mirror directory is empty
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-10-13 13:39:19 +02:00
Philip Laine
f9d2a10044
Fix append to backup always happening (#603) 2024-10-13 13:12:46 +02:00
Philip Laine
607179dee9
Fix append to backup always happening
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-10-12 12:26:59 +02:00
Philip Laine
5140f2f16c
Prepare v0.0.26 (#602) 2024-10-11 12:19:20 +02:00
Philip Laine
47519889e4
Prepare v0.0.26 2024-10-11 12:14:06 +02:00
Philip Laine
f2efc93595
Fix Containerd host mirror ordering (#601) 2024-10-11 11:55:46 +02:00
Philip Laine
a23c34137a
Fix Containerd host mirror ordering
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-10-11 11:49:50 +02:00
Philip Laine
0d8192e6b6
fix: Correct Max Request Duration in Grafana (#598) 2024-10-09 21:07:10 +02:00
Tim Collins
3bc13c7908
fix: Correct Max Request Duration in Grafana
Signed-off-by: Tim Collins <tim@thecollins.team>
2024-10-09 15:32:07 +01:00
Philip Laine
52863a4cb7
Remove throttling from blobs (#596) 2024-10-07 22:10:13 +02:00
Philip Laine
819c04957c
Remove throttling from blobs
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-10-07 22:02:48 +02:00
Philip Laine
6bb8a9e9bf
Bump golang.org/x/time from 0.6.0 to 0.7.0 (#595) 2024-10-07 14:36:09 +02:00
dependabot[bot]
88b7f65f48
Bump golang.org/x/time from 0.6.0 to 0.7.0
Bumps [golang.org/x/time](https://github.com/golang/time) from 0.6.0 to 0.7.0.
- [Commits](https://github.com/golang/time/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/time
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-07 12:27:43 +00:00
Philip Laine
353f24761b
Bump github.com/Masterminds/semver/v3 from 3.2.1 to 3.3.0 (#594) 2024-10-07 14:26:15 +02:00
dependabot[bot]
cb9107f746
Bump github.com/Masterminds/semver/v3 from 3.2.1 to 3.3.0
Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.2.1 to 3.3.0.
- [Release notes](https://github.com/Masterminds/semver/releases)
- [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Masterminds/semver/compare/v3.2.1...v3.3.0)

---
updated-dependencies:
- dependency-name: github.com/Masterminds/semver/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-07 12:14:46 +00:00
Philip Laine
03e2f07e6c
Bump github.com/libp2p/go-libp2p-kad-dht from 0.26.1 to 0.27.0 (#593) 2024-10-07 14:13:23 +02:00
dependabot[bot]
b8f8e214c7
Bump github.com/libp2p/go-libp2p-kad-dht from 0.26.1 to 0.27.0
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.26.1 to 0.27.0.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.26.1...v0.27.0)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-07 11:15:49 +00:00
Philip Laine
e2e7b4be50
Prepare release v0.0.25 (#592) 2024-10-07 10:21:05 +02:00
Philip Laine
9eec6abe74
prepare release v0.0.25 2024-10-07 10:14:17 +02:00
Philip Laine
4213d6fb7a
Update to Go v1.23.2 (#575) 2024-10-07 10:02:04 +02:00
Philip Laine
adf0e5bd0c
Update to Go v1.23.2
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-10-07 09:56:52 +02:00
Philip Laine
c1ccea9b36
Skip status response verification for containerd v2 (#581) 2024-10-07 09:52:14 +02:00
Jean-Francois Roy
e2ccf67aaf
fix: skip status response verification for containerd v2
Signed-off-by: Jean-Francois Roy <jf@devklog.net>
2024-10-07 00:18:15 -07:00
Philip Laine
a5895948b8
Bump sigstore/cosign-installer from 3.6.0 to 3.7.0 (#590) 2024-10-06 23:50:11 +02:00
dependabot[bot]
47a43b63ce
Bump sigstore/cosign-installer from 3.6.0 to 3.7.0
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.6.0...v3.7.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-04 11:33:21 +00:00
Philip Laine
309d82a2b4
Added the ability to optionally assign NODE_IP (#578) 2024-10-04 10:18:27 +02:00
Giacomo Consonni
93ab1b4bc4
Merge branch 'main' into main 2024-10-03 08:29:04 +02:00
Philip Laine
5e84b3c94f
Bump github.com/pelletier/go-toml/v2 from 2.2.2 to 2.2.3 (#568) 2024-09-29 16:39:24 +02:00
dependabot[bot]
8dac2b8675
Bump github.com/pelletier/go-toml/v2 from 2.2.2 to 2.2.3
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.2...v2.2.3)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-29 14:34:54 +00:00
Philip Laine
809c65d676
Bump go.etcd.io/bbolt from 1.3.10 to 1.3.11 (#565) 2024-09-29 16:33:22 +02:00
dependabot[bot]
79a09d8924
Bump go.etcd.io/bbolt from 1.3.10 to 1.3.11
Bumps [go.etcd.io/bbolt](https://github.com/etcd-io/bbolt) from 1.3.10 to 1.3.11.
- [Release notes](https://github.com/etcd-io/bbolt/releases)
- [Commits](https://github.com/etcd-io/bbolt/compare/v1.3.10...v1.3.11)

---
updated-dependencies:
- dependency-name: go.etcd.io/bbolt
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-29 14:28:05 +00:00
Philip Laine
9e0e8b8131
Bump github.com/containerd/containerd from 1.7.21 to 1.7.22 (#580) 2024-09-29 16:25:06 +02:00
dependabot[bot]
675012abfc
Bump github.com/containerd/containerd from 1.7.21 to 1.7.22
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.21 to 1.7.22.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.21...v1.7.22)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-29 14:20:00 +00:00
Philip Laine
2d23db7671
Bump k8s.io/client-go from 0.31.0 to 0.31.1 (#583) 2024-09-29 16:18:42 +02:00
dependabot[bot]
1f22baf2d2
Bump k8s.io/client-go from 0.31.0 to 0.31.1
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.31.0 to 0.31.1.
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.31.0...v0.31.1)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-29 14:14:01 +00:00
Philip Laine
e1b89a5bef
Bump k8s.io/cri-api from 0.31.0 to 0.31.1 (#584) 2024-09-29 16:12:45 +02:00
dependabot[bot]
4a584bb572
Bump k8s.io/cri-api from 0.31.0 to 0.31.1
Bumps [k8s.io/cri-api](https://github.com/kubernetes/cri-api) from 0.31.0 to 0.31.1.
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.31.0...v0.31.1)

---
updated-dependencies:
- dependency-name: k8s.io/cri-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-29 14:08:31 +00:00
Philip Laine
c27f96a77d
Bump github.com/prometheus/client_golang from 1.20.2 to 1.20.4 (#585) 2024-09-29 16:07:05 +02:00
dependabot[bot]
89728501e5
Bump github.com/prometheus/client_golang from 1.20.2 to 1.20.4
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.2 to 1.20.4.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.2...v1.20.4)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-17 11:19:53 +00:00
Giacomo Consonni
200ed4d537
feat: added the ability to optionally assign NODE_IP 2024-09-09 09:22:05 +02:00
Philip Laine
bce1b95923
Bump k8s.io/client-go from 0.30.3 to 0.31.0 (#559) 2024-09-01 18:38:35 +02:00
dependabot[bot]
acc93016e0
Bump k8s.io/client-go from 0.30.3 to 0.31.0
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.30.3 to 0.31.0.
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.30.3...v0.31.0)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 16:33:08 +00:00
Philip Laine
20336aa2d3
Bump k8s.io/cri-api from 0.30.3 to 0.31.0 (#560) 2024-09-01 18:31:47 +02:00
dependabot[bot]
b45cd5724a
Bump k8s.io/cri-api from 0.30.3 to 0.31.0
Bumps [k8s.io/cri-api](https://github.com/kubernetes/cri-api) from 0.30.3 to 0.31.0.
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.30.3...v0.31.0)

---
updated-dependencies:
- dependency-name: k8s.io/cri-api
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 15:58:54 +00:00
Philip Laine
a68dad14bf
Release v0.0.24 (#574) 2024-09-01 17:31:50 +02:00
Philip Laine
c5fee7e4cc
Prepare v0.0.24 2024-09-01 17:27:09 +02:00
Philip Laine
9126f7f4cc
Use buffer pool for proxy copying data (#573) 2024-09-01 17:23:39 +02:00
Philip Laine
2c0b6d00bd
Use buffer pool for proxy copying data
Signed-off-by: Philip Laine <philip.laine@gmail.com>
2024-09-01 17:17:41 +02:00
Philip Laine
81cfa529a6
Bump github.com/containerd/containerd from 1.7.20 to 1.7.21 (#570) 2024-08-28 08:50:23 +02:00
dependabot[bot]
12af8640a7
Bump github.com/containerd/containerd from 1.7.20 to 1.7.21
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.20 to 1.7.21.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.20...v1.7.21)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 11:22:21 +00:00
Philip Laine
914c26e5c2
Bump github.com/prometheus/client_golang from 1.19.1 to 1.20.2 (#569) 2024-08-27 11:09:29 +02:00
dependabot[bot]
7db821a09b
Bump github.com/prometheus/client_golang from 1.19.1 to 1.20.2
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.19.1 to 1.20.2.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.19.1...v1.20.2)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 09:05:30 +00:00
Philip Laine
7e4a32647a
Bump github.com/libp2p/go-libp2p-kad-dht from 0.25.2 to 0.26.1 (#566) 2024-08-27 11:04:11 +02:00
dependabot[bot]
b838389e82
Bump github.com/libp2p/go-libp2p-kad-dht from 0.25.2 to 0.26.1
Bumps [github.com/libp2p/go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht) from 0.25.2 to 0.26.1.
- [Release notes](https://github.com/libp2p/go-libp2p-kad-dht/releases)
- [Commits](https://github.com/libp2p/go-libp2p-kad-dht/compare/v0.25.2...v0.26.1)

---
updated-dependencies:
- dependency-name: github.com/libp2p/go-libp2p-kad-dht
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-22 11:23:42 +00:00
Philip Laine
77de00d660
Bump sigstore/cosign-installer from 3.5.0 to 3.6.0 (#558) 2024-08-08 14:19:43 +02:00
dependabot[bot]
9d0fe4b3d7
Bump sigstore/cosign-installer from 3.5.0 to 3.6.0
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.5.0...v3.6.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-08 11:38:58 +00:00
Philip Laine
333a40dcfd
feat(helm): add config for revisionHistoryLimit (#556) 2024-08-06 22:34:04 +02:00
t3mi
4bcc77d49a feat(helm): add config for revisionHistoryLimit
Signed-off-by: t3mi <t3mi@users.noreply.github.com>
2024-08-06 22:25:49 +02:00
Philip Laine
85230a40a5
feat(helm): re-use resources value for initContainer (#553) 2024-08-06 22:19:46 +02:00
t3mi
ed2b9a2f97 feat(helm): re-use resources value for initContainer
Signed-off-by: t3mi <t3mi@users.noreply.github.com>
2024-08-06 08:33:16 +00:00
Philip Laine
5d25fc8cc2
Bump k8s.io/client-go from 0.30.2 to 0.30.3 (#543) 2024-08-05 23:35:56 +02:00
dependabot[bot]
789471a3e8
Bump k8s.io/client-go from 0.30.2 to 0.30.3
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.30.2 to 0.30.3.
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.30.2...v0.30.3)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 21:30:25 +00:00
Philip Laine
6ea4356e46
Bump k8s.io/cri-api from 0.30.2 to 0.30.3 (#544) 2024-08-05 23:29:09 +02:00
dependabot[bot]
d6091c5ecf
Bump k8s.io/cri-api from 0.30.2 to 0.30.3
Bumps [k8s.io/cri-api](https://github.com/kubernetes/cri-api) from 0.30.2 to 0.30.3.
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.30.2...v0.30.3)

---
updated-dependencies:
- dependency-name: k8s.io/cri-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 21:23:38 +00:00
Philip Laine
20d78cb58c
Add support for vertical pod autoscaler (#552) 2024-08-05 23:22:25 +02:00
t3mi
52de5b9615 feat(helm): add support for vertical pod autoscaler
Signed-off-by: t3mi <t3mi@users.noreply.github.com>
2024-08-05 23:17:50 +02:00
Philip Laine
44240109f3
Bump golang.org/x/time from 0.5.0 to 0.6.0 (#554) 2024-08-05 23:17:12 +02:00
dependabot[bot]
98121d1965
Bump golang.org/x/time from 0.5.0 to 0.6.0
Bumps [golang.org/x/time](https://github.com/golang/time) from 0.5.0 to 0.6.0.
- [Commits](https://github.com/golang/time/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: golang.org/x/time
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 21:11:59 +00:00
Philip Laine
db9ea28525
Bump golang.org/x/sync from 0.7.0 to 0.8.0 (#555) 2024-08-05 23:10:36 +02:00
dependabot[bot]
17895d1cc7
Bump golang.org/x/sync from 0.7.0 to 0.8.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.7.0 to 0.8.0.
- [Commits](https://github.com/golang/sync/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 11:13:56 +00:00
Philip Laine
ecfdd57cb2
Set blob content type to disable detection (#547) 2024-07-27 13:35:44 +02:00
Philip Laine
27fa7d6066
Set blob content type to disable detection 2024-07-27 13:30:11 +02:00
Philip Laine
e030842414
Bump github.com/containerd/typeurl/v2 from 2.1.1 to 2.2.0 (#539) 2024-07-22 00:19:21 +02:00
dependabot[bot]
65fa623b17
Bump github.com/containerd/typeurl/v2 from 2.1.1 to 2.2.0
Bumps [github.com/containerd/typeurl/v2](https://github.com/containerd/typeurl) from 2.1.1 to 2.2.0.
- [Release notes](https://github.com/containerd/typeurl/releases)
- [Commits](https://github.com/containerd/typeurl/compare/v2.1.1...v2.2.0)

---
updated-dependencies:
- dependency-name: github.com/containerd/typeurl/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-21 22:14:36 +00:00
Philip Laine
fd3cfd4ae4
Bump github.com/containerd/containerd from 1.7.19 to 1.7.20 (#542) 2024-07-22 00:13:18 +02:00
dependabot[bot]
dc2f07cefb
Bump github.com/containerd/containerd from 1.7.19 to 1.7.20
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.19 to 1.7.20.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.19...v1.7.20)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-18 11:10:58 +00:00
Philip Laine
6d88873c09
Document tainting new nodes to wait for Spegel (#540) 2024-07-11 23:19:29 +02:00
Philip Laine
c6c945d5d0
Document tainting new nodes to wait for Spegel 2024-07-11 23:13:13 +02:00
Philip Laine
172b16890b
Replace mock OCI client with in memory client (#538) 2024-07-10 16:08:30 +02:00
Philip Laine
5756839a02
Replace mock OCI client with in memory client 2024-07-10 16:01:53 +02:00
Philip Laine
8d438de9ee
Bump github.com/containerd/containerd from 1.7.18 to 1.7.19 (#534) 2024-07-09 09:46:01 +02:00
dependabot[bot]
326bd991fc
Bump github.com/containerd/containerd from 1.7.18 to 1.7.19
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.18 to 1.7.19.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.18...v1.7.19)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-09 07:41:41 +00:00
Philip Laine
ccefbbe207
Bump k8s.io/klog/v2 from 2.100.1 to 2.130.1 (#521) 2024-07-09 09:40:39 +02:00
dependabot[bot]
6b957de996
Bump k8s.io/klog/v2 from 2.100.1 to 2.130.1
Bumps [k8s.io/klog/v2](https://github.com/kubernetes/klog) from 2.100.1 to 2.130.1.
- [Release notes](https://github.com/kubernetes/klog/releases)
- [Changelog](https://github.com/kubernetes/klog/blob/main/RELEASE.md)
- [Commits](https://github.com/kubernetes/klog/compare/v2.100.1...v2.130.1)

---
updated-dependencies:
- dependency-name: k8s.io/klog/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-09 07:35:19 +00:00
Philip Laine
9773e539c1
Bump k8s.io/cri-api from 0.28.8 to 0.30.2 (#512) 2024-07-09 09:26:11 +02:00
dependabot[bot]
30367dea52
Bump k8s.io/cri-api from 0.28.8 to 0.30.2
Bumps [k8s.io/cri-api](https://github.com/kubernetes/cri-api) from 0.28.8 to 0.30.2.
- [Commits](https://github.com/kubernetes/cri-api/compare/v0.28.8...v0.30.2)

---
updated-dependencies:
- dependency-name: k8s.io/cri-api
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-09 07:21:55 +00:00
Philip Laine
ce318ffa70
Bump k8s.io/client-go from 0.28.8 to 0.30.2 (#513) 2024-07-09 09:20:52 +02:00
dependabot[bot]
0d1c25e424
Bump k8s.io/client-go from 0.28.8 to 0.30.2
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.28.8 to 0.30.2.
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.28.8...v0.30.2)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-09 07:15:53 +00:00
Philip Laine
95019ce047
Bump github.com/norwoodj/helm-docs from 1.13.1 to 1.14.2 (#537) 2024-07-09 09:14:54 +02:00
dependabot[bot]
b8929a86bb
Bump github.com/norwoodj/helm-docs from 1.13.1 to 1.14.2
Bumps [github.com/norwoodj/helm-docs](https://github.com/norwoodj/helm-docs) from 1.13.1 to 1.14.2.
- [Release notes](https://github.com/norwoodj/helm-docs/releases)
- [Changelog](https://github.com/norwoodj/helm-docs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/norwoodj/helm-docs/compare/v1.13.1...v1.14.2)

---
updated-dependencies:
- dependency-name: github.com/norwoodj/helm-docs
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 11:45:35 +00:00
Philip Laine
60a6743dbb
Update Go version to 1.22.5 (#536) 2024-07-04 13:03:23 +02:00
Philip Laine
68106480f3
Update Go version to 1.22.5 2024-07-04 12:58:59 +02:00
Philip Laine
ca2a5d2b6d
Fix Docker build casing checks (#535) 2024-07-04 12:55:22 +02:00
Philip Laine
46eacdc221
Fix Docker build casing checks 2024-07-04 12:50:55 +02:00
Philip Laine
383f45a7b1
Extend tests for routing (#533) 2024-07-02 23:00:28 +02:00
Philip Laine
55d04a4d20
Extend tests for routing 2024-07-02 22:46:37 +02:00
Philip Laine
569f0a5f95
Add benchmarks for v0.0.23 (#532) 2024-07-02 21:29:55 +02:00
Philip Laine
21ca862d93
Add benchmarks for v0.0.23 2024-07-02 21:19:09 +02:00
Philip Laine
5f8d04244f
Bump github.com/multiformats/go-multiaddr from 0.12.4 to 0.13.0 (#527) 2024-07-02 20:55:26 +02:00
dependabot[bot]
603000a711
Bump github.com/multiformats/go-multiaddr from 0.12.4 to 0.13.0
Bumps [github.com/multiformats/go-multiaddr](https://github.com/multiformats/go-multiaddr) from 0.12.4 to 0.13.0.
- [Release notes](https://github.com/multiformats/go-multiaddr/releases)
- [Commits](https://github.com/multiformats/go-multiaddr/compare/v0.12.4...v0.13.0)

---
updated-dependencies:
- dependency-name: github.com/multiformats/go-multiaddr
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 20:28:52 +00:00
Philip Laine
87c7f610b6
Bump github.com/alexflint/go-arg from 1.5.0 to 1.5.1 (#531) 2024-07-01 22:27:55 +02:00
dependabot[bot]
67d9675c1a
Bump github.com/alexflint/go-arg from 1.5.0 to 1.5.1
Bumps [github.com/alexflint/go-arg](https://github.com/alexflint/go-arg) from 1.5.0 to 1.5.1.
- [Release notes](https://github.com/alexflint/go-arg/releases)
- [Commits](https://github.com/alexflint/go-arg/compare/v1.5.0...v1.5.1)

---
updated-dependencies:
- dependency-name: github.com/alexflint/go-arg
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 11:11:28 +00:00
Philip Laine
021cb4b015
chore(helm): allow relabeling rules for service monitor (#526) 2024-06-28 12:27:19 +02:00
eliasbokreta
0c2d2e9d15
chore: allow relabeling rules for service monitor 2024-06-27 09:35:17 +02:00
Philip Laine
995739f9e7
Merge pull request #523 from spegel-org/docs/gc
Add documentation for image garbage collection
2024-06-24 12:15:26 +02:00
Philip Laine
212266d479
Add documentation for image garbage collection 2024-06-24 10:36:40 +02:00
Philip Laine
91e4badd12
Merge pull request #520 from spegel-org/test/metrics
Add tests for metrics
2024-06-20 10:06:31 +02:00
Philip Laine
2e765d2f32
Add tests for metrics 2024-06-20 10:01:39 +02:00
Philip Laine
e0ac8418b8
Merge pull request #519 from spegel-org/test/containerd
Extend tests for containerd
2024-06-20 09:29:29 +02:00
Philip Laine
8342cd55ed
Extend tests for containerd 2024-06-20 09:23:37 +02:00
Philip Laine
da65403ee8
Merge pull request #518 from spegel-org/test/image
Extend tests for image
2024-06-19 23:09:12 +02:00
Philip Laine
b9e3d460f0
Extend tests for image 2024-06-19 23:04:32 +02:00
Philip Laine
6b2c51407f
Merge pull request #517 from spegel-org/remove/copy-layer
Remove deprecated CopyLayer function
2024-06-19 17:00:43 +02:00
Philip Laine
20b2f22907
Remove deprecated CopyLayer function 2024-06-19 16:54:20 +02:00
Philip Laine
81373b8c36
Merge pull request #516 from spegel-org/test/mux
Extend tests for mux
2024-06-19 16:46:48 +02:00
Philip Laine
1583633faa
extend tests for mux 2024-06-19 16:40:47 +02:00
Philip Laine
d2a6e94857
Merge pull request #514 from spegel-org/dependabot/github_actions/docker/build-push-action-6
Bump docker/build-push-action from 5 to 6
2024-06-17 21:09:07 +02:00
dependabot[bot]
a9a78ae663
Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 11:24:16 +00:00
Philip Laine
ebc320b15c
Merge pull request #511 from spegel-org/codecov
Upload code coverage result to Codecov
2024-06-11 18:40:30 +02:00
Philip Laine
6a38c20b9a
Upload code coverage result to Codecov 2024-06-11 18:34:41 +02:00
Philip Laine
6abe5caf43
Merge pull request #508 from spegel-org/prepare/v0.0.23
Release v0.0.23
2024-06-09 14:30:33 +02:00
Philip Laine
acd27fbe06
Release v0.0.23 2024-06-09 14:24:29 +02:00
Philip Laine
9725ab5dcf
Merge pull request #507 from spegel-org/resolve-timeout
Change default resolve timeout to 20ms
2024-06-09 14:14:48 +02:00
Philip Laine
56d820bb38
Change default resolve timeout to 20ms 2024-06-09 13:55:40 +02:00
Philip Laine
11d38df344
Merge pull request #506 from spegel-org/dependabot/go_modules/github.com/containerd/containerd-1.7.18
Bump github.com/containerd/containerd from 1.7.17 to 1.7.18
2024-06-08 10:55:36 +02:00
dependabot[bot]
49501f281b
Bump github.com/containerd/containerd from 1.7.17 to 1.7.18
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.17 to 1.7.18.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.17...v1.7.18)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-05 11:32:49 +00:00
Philip Laine
d69c1fba78
Merge pull request #505 from spegel-org/fix/e2e-image
Fix e2e test not found image
2024-06-05 09:30:51 +02:00
Philip Laine
40844d9b96
Fix e2e test not found iamge 2024-06-05 09:23:41 +02:00
Philip Laine
973721faf0
Merge pull request #501 from spegel-org/refactor/memory-router
Rename mock router to memory router and add tests
2024-05-23 09:44:44 +02:00
Philip Laine
4e580793e6
Rename mock router to memory router and add tests 2024-05-23 00:17:48 +02:00
Philip Laine
b547f0ecd2
Merge pull request #500 from spegel-org/fix/changelog-link
Fix changelog links
2024-05-22 22:27:42 +02:00
Philip Laine
5265bb41d8
Fix changelog links 2024-05-22 22:21:34 +02:00
Philip Laine
f7e31f82b1
Merge pull request #499 from spegel-org/feature/paralleltest
Add paralleltest linter and set all unit tests to run in parallel
2024-05-22 22:17:52 +02:00
Philip Laine
24647f2cb6
Add paralleltest linter and set all unit tests to run in parallel 2024-05-22 22:07:35 +02:00
Philip Laine
7cc94682a8
Merge pull request #380 from spegel-org/dependabot/go_modules/github.com/norwoodj/helm-docs-1.13.1
Bump github.com/norwoodj/helm-docs from 1.12.0 to 1.13.1
2024-05-22 19:39:48 +02:00
dependabot[bot]
8870947380
Bump github.com/norwoodj/helm-docs from 1.12.0 to 1.13.1
Bumps [github.com/norwoodj/helm-docs](https://github.com/norwoodj/helm-docs) from 1.12.0 to 1.13.1.
- [Release notes](https://github.com/norwoodj/helm-docs/releases)
- [Changelog](https://github.com/norwoodj/helm-docs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/norwoodj/helm-docs/compare/v1.12.0...v1.13.1)

---
updated-dependencies:
- dependency-name: github.com/norwoodj/helm-docs
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 17:05:14 +00:00
Philip Laine
290a6514c4
Merge pull request #498 from spegel-org/update/go-1.22
Update to Go 1.22
2024-05-22 19:03:50 +02:00
Philip Laine
6f6df21f66
Update to Go 1.22 2024-05-22 18:58:23 +02:00
Philip Laine
a4d3cebd0a
Merge pull request #497 from spegel-org/dependabot/go_modules/github.com/go-logr/logr-1.4.2
Bump github.com/go-logr/logr from 1.4.1 to 1.4.2
2024-05-22 14:27:36 +02:00
dependabot[bot]
295abf51db
---
updated-dependencies:
- dependency-name: github.com/go-logr/logr
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 11:48:22 +00:00
Philip Laine
21552b47a6
Merge pull request #464 from spegel-org/dependabot/go_modules/go.etcd.io/bbolt-1.3.10
Bump go.etcd.io/bbolt from 1.3.9 to 1.3.10
2024-05-22 08:51:29 +02:00
dependabot[bot]
df06608279
Bump go.etcd.io/bbolt from 1.3.9 to 1.3.10
Bumps [go.etcd.io/bbolt](https://github.com/etcd-io/bbolt) from 1.3.9 to 1.3.10.
- [Release notes](https://github.com/etcd-io/bbolt/releases)
- [Commits](https://github.com/etcd-io/bbolt/compare/v1.3.9...v1.3.10)

---
updated-dependencies:
- dependency-name: go.etcd.io/bbolt
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 06:33:41 +00:00
Philip Laine
9f55793d15
Merge pull request #492 from spegel-org/dependabot/go_modules/github.com/multiformats/go-multiaddr-0.12.4
Bump github.com/multiformats/go-multiaddr from 0.12.3 to 0.12.4
2024-05-22 08:32:41 +02:00
dependabot[bot]
74b21b4656
Bump github.com/multiformats/go-multiaddr from 0.12.3 to 0.12.4
Bumps [github.com/multiformats/go-multiaddr](https://github.com/multiformats/go-multiaddr) from 0.12.3 to 0.12.4.
- [Release notes](https://github.com/multiformats/go-multiaddr/releases)
- [Commits](https://github.com/multiformats/go-multiaddr/compare/v0.12.3...v0.12.4)

---
updated-dependencies:
- dependency-name: github.com/multiformats/go-multiaddr
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 06:26:28 +00:00
Philip Laine
605f0f06b3
Merge pull request #493 from spegel-org/dependabot/go_modules/github.com/containerd/containerd-1.7.17
Bump github.com/containerd/containerd from 1.7.16 to 1.7.17
2024-05-22 08:23:41 +02:00
dependabot[bot]
d78c19259a
Bump github.com/containerd/containerd from 1.7.16 to 1.7.17
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.16 to 1.7.17.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.16...v1.7.17)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 06:18:20 +00:00
Philip Laine
025ae725b6
Merge pull request #496 from spegel-org/fix/bootstrap-old-lease
Fix p2p bootstrap to run on failed readiness check
2024-05-21 09:26:17 +02:00
Philip Laine
02d8a881dd
Run bootsrap when not ready 2024-05-21 00:46:22 +02:00
Philip Laine
59228cb579
Merge pull request #495 from spegel-org/e2e-reuse
Modify e2e tests to allow reusing the same kind cluster
2024-05-21 00:16:46 +02:00
Philip Laine
856a633e78
Modify e2e tests to allow reusing the same kind cluster 2024-05-21 00:06:49 +02:00
Philip Laine
3d770a28da
Merge pull request #491 from spegel-org/fix/resolve-context
Fix so that resolve timeout does not cancel mirroring attempts
2024-05-17 00:03:06 +02:00
Philip Laine
f79104c44c
Fix so that resolve timeout does not cancel mirroring attempts 2024-05-16 23:55:52 +02:00
Philip Laine
0a908817ed
Merge pull request #490 from spegel-org/fix/immediate-channel
Close immediate channel after writing to it to close wait group in merge logic
2024-05-16 23:03:06 +02:00
Philip Laine
aa98f85e9d
Close immediate channel after writing to it to close wait group in merge logic 2024-05-16 22:45:36 +02:00
Philip Laine
9794808333
Merge pull request #489 from spegel-org/fix/changelog
Fix missing changelog entry
2024-05-16 22:37:11 +02:00
Philip Laine
df0a3c94f8
Fix missing changelog entry 2024-05-16 22:23:33 +02:00
Philip Laine
06ab49f404
Merge pull request #488 from spegel-org/registry-errors
Update existing registry errors and add more detail
2024-05-16 22:08:12 +02:00
Philip Laine
71d3c5544f
Update existing registry errors and add more detail 2024-05-16 21:21:37 +02:00
Philip Laine
6633882f75
Merge pull request #487 from spegel-org/mirror-metrics
Move mirror metrics code to mirror handler
2024-05-15 22:02:52 +02:00
Philip Laine
4aa96cf050
Move mirror metrics code to mirror handler 2024-05-15 21:51:17 +02:00
Philip Laine
6b1eebe3e8
Merge pull request #483 from spegel-org/lint/errcheck
Update errcheck linter configuration and fix errors
2024-05-15 10:18:04 +02:00
Philip Laine
0d3ed3fde4
Update errcheck linter configuration and fix errors 2024-05-15 10:11:25 +02:00
Philip Laine
7a64c3404c
Merge pull request #482 from spegel-org/lint/gocritic
Enable gocritic linter and fix errors
2024-05-15 10:07:55 +02:00
Philip Laine
afbe10e8ce
Enable gocritic linter and fix errors 2024-05-15 10:01:42 +02:00
Philip Laine
e9d01cb9fc
Merge pull request #481 from spegel-org/lint/perfsprint
Enable perfsprint linter and fix errors
2024-05-15 09:55:38 +02:00
Philip Laine
c18e3e5ad7
Enable perfsprint linter and fix errors 2024-05-14 23:15:28 +02:00
Philip Laine
230b5352b7
Merge pull request #480 from spegel-org/lint/ireturn
Enable ireturn linter and fix errors
2024-05-14 23:12:01 +02:00
Philip Laine
3d7571fb00
Enable ireturn linter and fix errors 2024-05-14 22:53:12 +02:00
Philip Laine
02b705986b
Merge pull request #479 from spegel-org/lint/goimports
Enable goimports linter and fix errors
2024-05-14 22:51:09 +02:00
Philip Laine
251d0d3c9a
Enable goimports linter and fix errors 2024-05-14 22:35:53 +02:00
Philip Laine
56de8f6962
Merge pull request #478 from spegel-org/ci-lint-config
Update Golang CI Lint configuration
2024-05-14 22:32:50 +02:00
Philip Laine
ad27e9230c
Update Golang CI Lint configuration 2024-05-14 22:26:19 +02:00
Philip Laine
84b5cec21d
Merge pull request #477 from spegel-org/refactor/distribution-ref
Refactor distribution ref to simplify registry routing
2024-05-14 10:27:41 +02:00
Philip Laine
ad5a391229
Refactor distribution ref to simplify registry routing 2024-05-14 10:18:33 +02:00
Philip Laine
c997b02c64
Merge pull request #475 from spegel-org/move-resolve-ref
Move resolving ref to digest to manifest handler
2024-05-14 09:35:14 +02:00
Philip Laine
d99ed74c14
Move resolving ref to digest to manifest handler 2024-05-14 09:20:11 +02:00
Philip Laine
f165d711cc
Merge pull request #476 from spegel-org/fix/actions
Remove path filters from github actions
2024-05-14 09:19:46 +02:00
Philip Laine
665996f345
Remove path filters from github actions 2024-05-14 09:13:22 +02:00
Philip Laine
6b8433927e
Merge pull request #474 from spegel-org/refactor/github-actions
Refactor GitHub actions to only trigger on specific file changes
2024-05-13 23:17:04 +02:00
Philip Laine
b3647d0b1d
Refactor GitHub actions to only trigger on specific file changes 2024-05-13 23:06:58 +02:00
Philip Laine
b25b142a8a
Merge pull request #471 from spegel-org/fix/logging-handler
Fix handler key in request logging
2024-05-13 22:36:18 +02:00
Philip Laine
d4b81d0646
Fix handler key in request logging 2024-05-13 21:50:58 +02:00
Philip Laine
dc42ec02e1
Merge pull request #473 from spegel-org/dependabot/go_modules/github.com/alexflint/go-arg-1.5.0
Bump github.com/alexflint/go-arg from 1.4.3 to 1.5.0
2024-05-13 21:42:10 +02:00
dependabot[bot]
4c570127eb
Bump github.com/alexflint/go-arg from 1.4.3 to 1.5.0
Bumps [github.com/alexflint/go-arg](https://github.com/alexflint/go-arg) from 1.4.3 to 1.5.0.
- [Release notes](https://github.com/alexflint/go-arg/releases)
- [Commits](https://github.com/alexflint/go-arg/compare/v1.4.3...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/alexflint/go-arg
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 11:22:13 +00:00
Philip Laine
261accbddc
Merge pull request #470 from spegel-org/dependabot/go_modules/github.com/prometheus/client_golang-1.19.1
Bump github.com/prometheus/client_golang from 1.19.0 to 1.19.1
2024-05-10 15:29:23 +02:00
dependabot[bot]
8ed7ab978a
Bump github.com/prometheus/client_golang from 1.19.0 to 1.19.1
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-10 12:02:59 +00:00
Philip Laine
39a9f51341
Merge pull request #468 from spegel-org/go-version
Fetch go version from mod file
2024-05-07 23:35:11 +02:00
Philip Laine
882f7eda23
Fetch go version from mod file 2024-05-07 23:25:55 +02:00
Philip Laine
90612c3100
Merge pull request #467 from spegel-org/update/golang-ci-lint
Update golang ci lint version
2024-05-07 23:24:21 +02:00
Philip Laine
399cb77122
Update golang ci lint version 2024-05-07 23:17:28 +02:00
Philip Laine
d8045a9441
Merge pull request #466 from spegel-org/dependabot/github_actions/golangci/golangci-lint-action-6
Bump golangci/golangci-lint-action from 5 to 6
2024-05-07 13:50:04 +02:00
dependabot[bot]
8918d1e2d2
Bump golangci/golangci-lint-action from 5 to 6
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 5 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-07 11:42:48 +00:00
Philip Laine
01fe64d954
Merge pull request #458 from bittrance/dev-docs
Basic instructions on building and starting a local environment
2024-05-07 12:29:38 +02:00
Bittrance
7e2a4bb588
Fix image build instruction in contribution guidelines. 2024-05-07 10:46:46 +02:00
Bittrance
0b5611fe80
Move local testing instructions to contributing guidelines. 2024-05-07 10:46:46 +02:00
Bittrance
f3cb13df06
Basic instructions on building and starting dev env. 2024-05-07 10:46:45 +02:00
Philip Laine
456cded36c
Merge pull request #465 from spegel-org/fix/makefile-image
Fix Makefile so that both image and tag can be configured
2024-05-07 10:10:01 +02:00
Philip Laine
406d5a803e
Fix Makefile so that both image and tag can be configured 2024-05-07 10:01:24 +02:00
Philip Laine
490c549a39
Merge pull request #462 from bittrance/kind-compatibility
Kind compatibility notice
2024-05-02 21:31:19 +02:00
Philip Laine
9b8772489a
Merge branch 'main' into kind-compatibility 2024-05-02 20:54:02 +02:00
Bittrance
3a56f35ce0
Meeting phillebaba comments on #462. 2024-05-02 16:28:21 +02:00
Philip Laine
e9813a6b9d
Merge pull request #463 from spegel-org/dependabot/go_modules/github.com/pelletier/go-toml/v2-2.2.2
Bump github.com/pelletier/go-toml/v2 from 2.2.1 to 2.2.2
2024-05-02 15:17:02 +02:00
dependabot[bot]
8d9fddb871
Bump github.com/pelletier/go-toml/v2 from 2.2.1 to 2.2.2
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.1...v2.2.2)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 13:09:41 +00:00
Philip Laine
770ed80652
Merge pull request #457 from spegel-org/dependabot/go_modules/github.com/containerd/containerd-1.7.16
Bump github.com/containerd/containerd from 1.7.15 to 1.7.16
2024-05-02 15:07:38 +02:00
dependabot[bot]
24e14da1c6
Bump github.com/containerd/containerd from 1.7.15 to 1.7.16
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.15 to 1.7.16.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.15...v1.7.16)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 13:00:35 +00:00
Philip Laine
e57822a471
Merge pull request #456 from spegel-org/dependabot/github_actions/golangci/golangci-lint-action-5
Bump golangci/golangci-lint-action from 4 to 5
2024-05-02 14:57:56 +02:00
dependabot[bot]
08a141a28a
Bump golangci/golangci-lint-action from 4 to 5
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 5.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 12:51:26 +00:00
Philip Laine
b3cfb11813
Merge pull request #460 from stenh0use/fix-http-bootstrap-addr-env
Fix http-bootstrap-addr env variable
2024-05-02 14:49:36 +02:00
Bittrance
8d2cdd7e00
Kind compatibility notice. 2024-04-30 11:00:17 +02:00
stenh0use
0b8ee78311 Fix http-bootstrap-addr env variable 2024-04-27 15:38:26 -04:00
Philip Laine
836db6dc44
Merge pull request #455 from spegel-org/benchmark-22
Benchmark v0.0.22
2024-04-25 14:11:09 +02:00
Philip Laine
b6ae249813 Benchmark v0.0.22 2024-04-25 13:51:03 +02:00
Philip Laine
6d4b345657
Merge pull request #388 from onedr0p/grafana-dashboard
feat: add grafana dashboard to helm chart
2024-04-25 08:41:47 +02:00
Devin Buhl
29c8248cd3
Merge branch 'main' into grafana-dashboard 2024-04-23 15:48:35 -04:00
Philip Laine
eb0fbdaf63
Merge pull request #453 from spegel-org/release/v0.0.22
Release v0.0.22
2024-04-23 21:25:21 +02:00
Philip Laine
9730999288 Release v0.0.22 2024-04-23 21:00:26 +02:00
Philip Laine
fb0dcc771f
Merge pull request #452 from spegel-org/fix/subscribe-error
Fix Containerd subscribe returning on any error
2024-04-23 20:51:43 +02:00
Philip Laine
90fb28d8b0 Fix subscribe stopping to work when error occurs 2024-04-23 17:54:37 +02:00
Philip Laine
3071530c0e
Merge pull request #451 from spegel-org/dependabot/go_modules/golang.org/x/net-0.23.0
Bump golang.org/x/net from 0.21.0 to 0.23.0
2024-04-23 14:04:03 +02:00
dependabot[bot]
171f7eb1dd Bump golang.org/x/net from 0.21.0 to 0.23.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.21.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.21.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-23 13:57:01 +02:00
Philip Laine
2878800560
Merge pull request #450 from spegel-org/doc/benchmark
Update benchmark documentation
2024-04-23 13:50:55 +02:00
Philip Laine
1e5a72ae26 Update benchmark documentation 2024-04-23 13:33:23 +02:00
Philip Laine
3ac24e40db
Merge pull request #449 from spegel-org/feature/slog
Replace zapr with slog
2024-04-20 13:30:13 +02:00
Philip Laine
8ac5469c94 Replace zapr with slog 2024-04-20 13:18:05 +02:00
Philip Laine
a2bd6ce16f
Merge pull request #447 from guettli/patch-1
Update FAQ.md
2024-04-19 13:02:14 +02:00
Thomas Güttler
2205c33476
Update FAQ.md
fixed small typo.
2024-04-18 17:55:47 +02:00
Philip Laine
2380dc95a3
Merge pull request #443 from bittrance/failure-faq
FAQ answer for instance failure
2024-04-18 16:32:03 +02:00
Bittrance
f34da354f7
FAQ answer for instance failure. 2024-04-17 23:22:13 +02:00
Philip Laine
92645fbf07
Merge pull request #439 from spegel-org/fix/go-toolchain
Update Go version and fix toolchain version
2024-04-16 20:30:10 +02:00
Philip Laine
a0dff2d341 Update Go version and fix toolchain version 2024-04-16 20:23:38 +02:00
Philip Laine
7c70f7b1a2
Merge pull request #438 from spegel-org/host-path-type
Set host path type for Containerd socket
2024-04-15 14:56:26 +02:00
Philip Laine
2df5187898 Set host path type for Containerd socket 2024-04-15 14:15:08 +02:00
Philip Laine
cb491a04df
Merge pull request #437 from spegel-org/dependabot/go_modules/github.com/pelletier/go-toml/v2-2.2.1
Bump github.com/pelletier/go-toml/v2 from 2.2.0 to 2.2.1
2024-04-15 14:04:15 +02:00
dependabot[bot]
271789d271
Bump github.com/pelletier/go-toml/v2 from 2.2.0 to 2.2.1
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-15 11:12:18 +00:00
Devin Buhl
dd2bf54b17
Merge branch 'main' into grafana-dashboard 2024-04-02 14:02:39 -04:00
Devin Buhl
16b203594a
Merge branch 'main' into grafana-dashboard 2024-03-28 13:44:29 -04:00
Devin Buhl
ed37b4a34c
fix: run make helm-docs
Signed-off-by: Devin Buhl <devin@buhl.casa>
2024-03-23 09:42:24 -04:00
Devin Buhl
853fee6b61
Merge branch 'main' into grafana-dashboard 2024-03-23 09:40:15 -04:00
Devin Buhl
09fc266395
Update CHANGELOG.md 2024-03-23 09:19:27 -04:00
Devin Buhl
6b7d16cf3d
fix: run make helmdocs
Signed-off-by: Devin Buhl <devin@buhl.casa>
2024-03-23 08:59:32 -04:00
Devin Buhl
6ce6369ab0
Merge branch 'main' into grafana-dashboard 2024-03-22 08:47:42 -04:00
Devin Buhl
38b3762cf2
fix: allow annotations in values 2024-03-22 08:46:53 -04:00
Devin Buhl
7c7c3308be
fix: allow annotations in template 2024-03-22 08:46:16 -04:00
Devin Buhl
04a118b9f4
Merge branch 'main' into grafana-dashboard 2024-03-20 08:33:47 -04:00
Devin Buhl
38143e393a
fix: only chomp when needed
Signed-off-by: Devin Buhl <devin@buhl.casa>
2024-03-17 15:28:58 -04:00
Devin Buhl
bfae9c01b6
fix: create is not enabled
Signed-off-by: Devin Buhl <devin@buhl.casa>
2024-03-17 15:27:52 -04:00
Devin Buhl
26d70e9660
fix: configmap name not a knob
Signed-off-by: Devin Buhl <devin@buhl.casa>
2024-03-17 15:26:30 -04:00
Devin Buhl
403ec58414
fix: namespace not a knob
Signed-off-by: Devin Buhl <devin@buhl.casa>
2024-03-17 15:18:38 -04:00
Devin Buhl
4c3be69fea
feat: add grafana dashboard to helm chart
Signed-off-by: Devin Buhl <devin@buhl.casa>
2024-03-17 15:16:52 -04:00
122 changed files with 8440 additions and 11732 deletions

View File

@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill ot this bug report! Please read the [FAQ](../../docs/FAQ.md) and check existing issues before submitting.
Thank you for taking the time to fill ot this bug report! Please read the [FAQ](https://spegel.dev/docs/faq/) and check existing issues before submitting a new issue.
- type: input
attributes:
label: Spegel version

View File

@ -10,3 +10,7 @@ updates:
schedule:
interval: "daily"
open-pull-requests-limit: 15
groups:
k8s:
patterns:
- "k8s.io/*"

View File

@ -1,29 +1,30 @@
name: artifacthub
on:
push:
branches: ["main"]
paths: ["charts/spegel/artifacthub-repo.yml"]
paths:
- "charts/spegel/artifacthub-repo.yml"
permissions:
contents: read
packages: write
defaults:
run:
shell: bash
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
submodules: true
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup ORAS
uses: oras-project/setup-oras@v1
uses: oras-project/setup-oras@8d34698a59f5ffe24821f0b48ab62a3de8b64b20 #v1.2.3
- name: Push Artifact Hub metadata
run: oras push ghcr.io/spegel-org/helm-charts/spegel:artifacthub.io --config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml charts/spegel/artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml

38
.github/workflows/e2e.yaml vendored Normal file
View File

@ -0,0 +1,38 @@
name: e2e
on:
pull_request:
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- proxy-mode: iptables
ip-family: ipv4
- proxy-mode: iptables
ip-family: ipv6
- proxy-mode: ipvs
ip-family: ipv4
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 #v5.5.0
with:
go-version-file: go.mod
- name: Setup GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
install-only: true
- name: Setup Kind
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 #v1.12.0
with:
version: v0.29.0
install_only: true
- name: Run e2e
run: make test-e2e E2E_PROXY_MODE=${{ matrix.proxy-mode }} E2E_IP_FAMILY=${{ matrix.ip-family }}

35
.github/workflows/go.yaml vendored Normal file
View File

@ -0,0 +1,35 @@
name: go
on:
pull_request:
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 #v5.5.0
with:
go-version-file: go.mod
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 #v8.0.0
unit:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 #v5.5.0
with:
go-version-file: go.mod
- name: Run tests
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 #v5.4.3
with:
token: ${{ secrets.CODECOV_TOKEN }}

27
.github/workflows/helm.yaml vendored Normal file
View File

@ -0,0 +1,27 @@
name: helm
on:
pull_request:
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 #v5.5.0
with:
go-version-file: go.mod
- name: Run helm-docs
run: make helm-docs
- name: Check if working tree is dirty
run: |
if [[ $(git diff --stat) != '' ]]; then
git diff
echo 'run make helm-docs and commit changes'
exit 1
fi

View File

@ -1,55 +1,61 @@
name: release
on:
release:
types: [published]
push:
tags:
- 'v*'
permissions:
contents: read
contents: write
packages: write
id-token: write
defaults:
run:
shell: bash
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup Cosign
uses: sigstore/cosign-installer@v3.5.0
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb #v3.8.2
- name: Setup Helm
uses: azure/setup-helm@v4
uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 #v4.3.0
with:
version: v3.12.1
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
version: v3.17.3
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 #v3.10.0
- name: Setup yq
uses: frenck/action-setup-yq@v1
uses: frenck/action-setup-yq@c4b5be8b4a215c536a41d436757d9feb92836d4f #v1.0.2
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare
- name: Prepare version
id: prep
run: |
VERSION=sha-${GITHUB_SHA::8}
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF/refs\/tags\//}
fi
echo "Refer to the [Changelog](https://github.com/spegel-org/spegel/blob/main/CHANGELOG.md#${VERSION//.}) for list of changes." > ${{ runner.temp }}/NOTES.txt
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
args: release --clean --release-notes ${{ runner.temp }}/NOTES.txt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate images meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 #v5.7.0
with:
images: ghcr.io/spegel-org/spegel
images: ghcr.io/${{ github.repository_owner }}/spegel
tags: type=raw,value=${{ steps.prep.outputs.VERSION }}
- name: Publish multi-arch image
uses: docker/build-push-action@v5
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
id: build
with:
push: true
@ -57,20 +63,22 @@ jobs:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm/v7,linux/arm64
tags: ghcr.io/spegel-org/spegel:${{ steps.prep.outputs.VERSION }}
tags: ghcr.io/${{ github.repository_owner }}/spegel:${{ steps.prep.outputs.VERSION }}
labels: ${{ steps.meta.outputs.labels }}
- name: Sign the image with Cosign
run: |
cosign sign --yes ghcr.io/spegel-org/spegel@${{ steps.build.outputs.DIGEST }}
cosign sign --yes ghcr.io/${{ github.repository_owner }}/spegel@${{ steps.build.outputs.DIGEST }}
- name: Publish Helm chart to GHCR
id: helm
run: |
HELM_VERSION=${{ steps.prep.outputs.VERSION }}
HELM_VERSION=${HELM_VERSION#v}
rm charts/spegel/artifacthub-repo.yml
yq -i '.image.digest = "${{ steps.build.outputs.DIGEST }}"' charts/spegel/values.yaml
helm package --app-version ${{ steps.prep.outputs.VERSION }} --version ${{ steps.prep.outputs.VERSION }} charts/spegel
helm push spegel-${{ steps.prep.outputs.VERSION }}.tgz oci://ghcr.io/spegel-org/helm-charts 2> .digest
helm package --app-version ${{ steps.prep.outputs.VERSION }} --version ${HELM_VERSION} charts/spegel
helm push spegel-${HELM_VERSION}.tgz oci://ghcr.io/${{ github.repository_owner }}/helm-charts 2> .digest
DIGEST=$(cat .digest | awk -F "[, ]+" '/Digest/{print $NF}')
echo "DIGEST=${DIGEST}" >> $GITHUB_OUTPUT
- name: Sign the Helm chart with Cosign
run: |
cosign sign --yes ghcr.io/spegel-org/helm-charts/spegel@${{ steps.helm.outputs.DIGEST }}
cosign sign --yes ghcr.io/${{ github.repository_owner }}/helm-charts/spegel@${{ steps.helm.outputs.DIGEST }}

View File

@ -1,75 +0,0 @@
name: tests
on:
pull_request:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.21.x
check-latest: true
cache: true
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v4
with:
version: v1.55.2
args: --timeout 3m0s
unit:
needs: lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.21.x
check-latest: true
cache: true
- name: Run tests
run: make test
e2e:
needs: lint
runs-on: ubuntu-latest
strategy:
matrix:
cni: [iptables, iptables-ipv6, ipvs]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.21.x
check-latest: true
cache: true
- name: Run e2e
run: make e2e CNI=${{ matrix.cni }}
helm-docs:
needs: lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.21.x
check-latest: true
cache: true
- name: Run helm-docs
run: make helm-docs
- name: Check if working tree is dirty
run: |
if [[ $(git diff --stat) != '' ]]; then
git diff
echo 'run make helm-docs and commit changes'
exit 1
fi

3
.gitignore vendored
View File

@ -19,3 +19,6 @@
# Go workspace file
go.work
# Added by goreleaser init:
dist/

View File

@ -1,15 +1,105 @@
version: "2"
linters:
disable-all: true
default: none
enable:
- errcheck
- gosimple
- gocritic
- govet
- importas
- ineffassign
- staticcheck
- unused
- ireturn
- misspell
- nolintlint
- paralleltest
- perfsprint
- staticcheck
- testifylint
linters-settings:
govet:
enable:
- fieldalignment
- unused
- noctx
settings:
errcheck:
disable-default-exclusions: true
check-type-assertions: true
check-blank: true
gocritic:
enable-all: true
disabled-checks:
- importShadow
- hugeParam
- rangeValCopy
- whyNoLint
- unnamedResult
- httpNoBody
govet:
disable:
- shadow
enable-all: true
importas:
alias:
- pkg: io/fs
alias: iofs
- pkg: github.com/go-logr/logr/testing
alias: tlog
- pkg: github.com/pelletier/go-toml/v2/unstable
alias: tomlu
- pkg: github.com/multiformats/go-multiaddr/net
alias: manet
- pkg: github.com/multiformats/go-multiaddr
alias: ma
- pkg: github.com/multiformats/go-multicodec
alias: mc
- pkg: github.com/multiformats/go-multihash
alias: mh
- pkg: github.com/ipfs/go-cid
alias: cid
- pkg: github.com/libp2p/go-libp2p-kad-dht
alias: dht
- pkg: github.com/libp2p/go-libp2p/p2p/net/mock
alias: mocknet
- pkg: go.etcd.io/bbolt
alias: bolt
- pkg: k8s.io/cri-api/pkg/apis/runtime/v1
alias: runtimeapi
- pkg: github.com/containerd/containerd/api/events
alias: eventtypes
- pkg: github.com/opencontainers/go-digest
alias: digest
- pkg: github.com/opencontainers/image-spec/specs-go/v1
alias: ocispec
- pkg: k8s.io/apimachinery/pkg/util/version
alias: utilversion
no-extra-aliases: true
nolintlint:
require-explanation: true
require-specific: true
perfsprint:
strconcat: false
testifylint:
enable-all: true
ireturn:
allow:
- anon
- error
- empty
- stdlib
- github.com/libp2p/go-libp2p/core/crypto.PrivKey
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

26
.goreleaser.yaml Normal file
View File

@ -0,0 +1,26 @@
version: 2
project_name: spegel
before:
hooks:
- go mod tidy
builds:
- goos:
- linux
goarch:
- amd64
- arm
- arm64
goarm:
- 7
env:
- CGO_ENABLED=0
flags:
- -trimpath
- -a
no_unique_dist_dir: true
binary: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}/{{ .ProjectName }}"
archives:
- formats: [tar.gz]
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- none*

View File

@ -1,9 +0,0 @@
# Adopters
This list shows adopters of Spegel. If you are using Spegel in your organization, please consider yourself to this list, as it lends credibility to the project.
| Organization | Website |
| --- |--- |
| Xenit AB | https://xenit.se/ |
| National Research Platform | https://nationalresearchplatform.org |
| K3S | https://k3s.io/ |

View File

@ -7,24 +7,296 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
### Added
- [#435](https://github.com/spegel-org/spegel/pull/435) Add pprof endpoints to enable profiling.
- [#434](https://github.com/spegel-org/spegel/pull/434) Add optional Containerd local content store to increase serve performance.
- [#905](https://github.com/spegel-org/spegel/pull/905) Change mirror type to url and add byte range parameter.
- [#909](https://github.com/spegel-org/spegel/pull/909) Add base http client and transport.
- [#910](https://github.com/spegel-org/spegel/pull/910) Add drain and close function.
### Changed
- [#906](https://github.com/spegel-org/spegel/pull/906) Replace HTTP header strings with httpx constants.
- [#916](https://github.com/spegel-org/spegel/pull/916) Refactor OCI client options and add header configuration.
### Deprecated
### Removed
### Fixed
- [#911](https://github.com/spegel-org/spegel/pull/911) Enforce use of request contexts and fix response closing.
- [#914](https://github.com/spegel-org/spegel/pull/914) Fix OCI client header parsing and improve tests.
### Security
## v0.3.0
### Added
- [#877](https://github.com/spegel-org/spegel/pull/877) Add support for www authenticate header.
- [#878](https://github.com/spegel-org/spegel/pull/878) Add dial timeout configuration in Containerd mirror configuration.
- [#889](https://github.com/spegel-org/spegel/pull/889) Add support for content create events.
### Changed
- [#881](https://github.com/spegel-org/spegel/pull/881) Add Variable for job name in Grafana Dashboard.
- [#852](https://github.com/spegel-org/spegel/pull/852) Remove use of Afero in Containerd config.
- [#854](https://github.com/spegel-org/spegel/pull/854) Implement unit tests for cleanup logic.
- [#860](https://github.com/spegel-org/spegel/pull/860) Update Go to 1.24.2.
- [#864](https://github.com/spegel-org/spegel/pull/864) Rename OCI client to store.
- [#871](https://github.com/spegel-org/spegel/pull/871) Implement OCI client and refactor debug web pulling.
- [#873](https://github.com/spegel-org/spegel/pull/873) Refactor web to use internal mux router.
- [#875](https://github.com/spegel-org/spegel/pull/875) Change debug unit formatting and add totals.
- [#880](https://github.com/spegel-org/spegel/pull/880) Refactor store advertisement to list content.
- [#888](https://github.com/spegel-org/spegel/pull/888) Refactor OCI events to support content events.
- [#890](https://github.com/spegel-org/spegel/pull/890) Refactor Containerd options to use config struct.
- [#896](https://github.com/spegel-org/spegel/pull/896) Rename package mux to httpx and refactor http helpers.
- [#897](https://github.com/spegel-org/spegel/pull/897) Add descriptor to header conversion.
### Fixed
- [#869](https://github.com/spegel-org/spegel/pull/869) Fix request logging for redirects and not found pages.
- [#872](https://github.com/spegel-org/spegel/pull/872) Allow returning libp2p crypto priv key in linter.
- [#894](https://github.com/spegel-org/spegel/pull/894) Update Kind to v0.29.0 and Fix Containerd v2 support.
- [#899](https://github.com/spegel-org/spegel/pull/899) Handle situation where digest is missing in reigstry response header.
- [#902](https://github.com/spegel-org/spegel/pull/902) Disable data dir when running Spegel in Kubernetes.
## v0.2.0
### Added
- [#832](https://github.com/spegel-org/spegel/pull/832) Add delete hook to cleanup configuration from host when chart is uninstalled.
- [#846](https://github.com/spegel-org/spegel/pull/846) Build binaries as part of the release process.
- [#848](https://github.com/spegel-org/spegel/pull/848) Add support for a static bootstrapper.
- [#850](https://github.com/spegel-org/spegel/pull/850) Persist libp2p key to disk when data directory is set.
### Changed
- [#812](https://github.com/spegel-org/spegel/pull/812) Upgrade to Go 1.24.1 and switch to use go tool for helm docs.
- [#725](https://github.com/spegel-org/spegel/pull/725) Remove use of httputil reverse proxy.
- [#820](https://github.com/spegel-org/spegel/pull/820) Switch to using new test context.
- [#827](https://github.com/spegel-org/spegel/pull/827) Add p2p options to router for optional configuration.
- [#835](https://github.com/spegel-org/spegel/pull/835) Refactor registry config to align with router config.
- [#847](https://github.com/spegel-org/spegel/pull/847) Set default values for address arguments.
### Removed
- [#831](https://github.com/spegel-org/spegel/pull/831) Remove local address check when resolving peers.
### Fixed
- [#824](https://github.com/spegel-org/spegel/pull/824) Fix improper image string formatting and expand tests.
- [#825](https://github.com/spegel-org/spegel/pull/825) Fix gopls modernize warnings.
- [#826](https://github.com/spegel-org/spegel/pull/826) Standardize router channel naming.
- [#844](https://github.com/spegel-org/spegel/pull/844) Fix p2p option naming to conform with the standard.
- [#849](https://github.com/spegel-org/spegel/pull/849) Fix libp2p options so field is exported in configuration.
## v0.1.1
### Fixed
- [#807](https://github.com/spegel-org/spegel/pull/807) Update golangci lint and fix new issues.
- [#810](https://github.com/spegel-org/spegel/pull/810) Increase timeout to avoid flakiness in conformance tests.
- [#806](https://github.com/spegel-org/spegel/pull/806) Fix verification of Containerd configuration with suffixes.
## v0.1.0
### Added
- [#717](https://github.com/spegel-org/spegel/pull/717) Extend tests for distribution.
- [#753](https://github.com/spegel-org/spegel/pull/753) Set GOMAXPROCS and GOMEMLIMIT when limits are set.
- [#792](https://github.com/spegel-org/spegel/pull/792) Add dev deploy recipe to simplify local development.
- [#791](https://github.com/spegel-org/spegel/pull/791) Add debug view to help validating Spegel.
### Changed
- [#747](https://github.com/spegel-org/spegel/pull/747) Update Go to 1.23.6.
- [#750](https://github.com/spegel-org/spegel/pull/750) Rename append mirrors to prepend existing.
- [#373](https://github.com/spegel-org/spegel/pull/373) Apply mirror configuration on all registires by default.
- [#762](https://github.com/spegel-org/spegel/pull/762) Set appropriate buckets for response size
- [#778](https://github.com/spegel-org/spegel/pull/778) Replace interface{} with any alias.
- [#784](https://github.com/spegel-org/spegel/pull/784) Refactor distribution and move to OCI package.
- [#787](https://github.com/spegel-org/spegel/pull/787) Refactor OCI image to allow parsing without digest.
- [#794](https://github.com/spegel-org/spegel/pull/794) Set default memory request and limit in Helm chart.
### Removed
- [#796](https://github.com/spegel-org/spegel/pull/796) Remove name from OCI image struct.
- [#799](https://github.com/spegel-org/spegel/pull/799) Remove Kubernetes bootstrapper.
### Fixed
- [#743](https://github.com/spegel-org/spegel/pull/743) Remove metrics label from bootstrap service in Helm chart.
- [#748](https://github.com/spegel-org/spegel/pull/748) Fix topology annotation.
- [#785](https://github.com/spegel-org/spegel/pull/785) Fix verification of digests when parsing distribution path.
- [#798](https://github.com/spegel-org/spegel/pull/798) Restart Spegel if Containerd event subscription is disconnected.
- [#800](https://github.com/spegel-org/spegel/pull/800) Fix so that host is closed even when a bootstrap error occurs.
- [#801](https://github.com/spegel-org/spegel/pull/801) Fix helm values naming for additionalMirrorTargets and mirroredRegistries.
## v0.0.30
### Changed
- [#694](https://github.com/spegel-org/spegel/pull/694) Replace IP in multi address with manet.
- [#693](https://github.com/spegel-org/spegel/pull/693) Add commonLabels for pods.
- [#699](https://github.com/spegel-org/spegel/pull/699) Remove as mismatch error and replace with errors as.
- [#701](https://github.com/spegel-org/spegel/pull/701) Rewrite e2e tests in Go.
- [#704](https://github.com/spegel-org/spegel/pull/704) Update Containerd client to v2.
### Fixed
- [#689](https://github.com/spegel-org/spegel/pull/689) Make cluster domain configurable.
- [#696](https://github.com/spegel-org/spegel/pull/696) Fix DNS bootstrap self check.
- [#702](https://github.com/spegel-org/spegel/pull/702) Refactor and add tests for p2p ready.
- [#703](https://github.com/spegel-org/spegel/pull/703) Fix p2p router close panic and add tests.
## v0.0.29
### Added
- [#678](https://github.com/spegel-org/spegel/pull/678) Add support for setting common labels in Helm chart.
- [#681](https://github.com/spegel-org/spegel/pull/681) Add import as linter.
### Changed
- [#683](https://github.com/spegel-org/spegel/pull/683) Change bootstrapper to allow returning multiple peers.
- [#684](https://github.com/spegel-org/spegel/pull/684) Allow bootstrappers to return multiaddress only containing IP.
- [#680](https://github.com/spegel-org/spegel/pull/680) Switch to using headless service for bootstrapping.
## v0.0.28
### Added
- [#576](https://github.com/spegel-org/spegel/pull/576) Add support for range requests for blobs.
- [#621](https://github.com/spegel-org/spegel/pull/621) Added Mermaid diagrams documentation to help explain Spegel's inner workings.
- [#629](https://github.com/spegel-org/spegel/pull/629) Document how to use multiple Spegel deployments in the same cluster.
- [#661](https://github.com/spegel-org/spegel/pull/661) Add allocs to pprof endpoints.
### Changed
- [#608](https://github.com/spegel-org/spegel/pull/608) Use custom proxy transport and increase idle connections per host.
### Fixed
- [#651](https://github.com/spegel-org/spegel/pull/651) Fix Containerd CRI config verification.
- [#660](https://github.com/spegel-org/spegel/pull/660) Add accept ranges header to blob HEAD request.
## v0.0.27
### Fixed
- [#603](https://github.com/spegel-org/spegel/pull/603) Fix append to backup always happening.
- [#604](https://github.com/spegel-org/spegel/pull/604) Create empty backup directory when mirror directory is empty.
## v0.0.26
### Removed
- [#596](https://github.com/spegel-org/spegel/pull/596) Remove throttling from blobs.
### Fixed
- [#601](https://github.com/spegel-org/spegel/pull/601) Fix Containerd host mirror ordering.
## v0.0.25
### Added
- [#578](https://github.com/spegel-org/spegel/pull/578) Add possibility to override environment variable NODE_IP.
### Changed
- [#575](https://github.com/spegel-org/spegel/pull/575) Update to Go v1.23.2.
### Fixed
- [#581](https://github.com/spegel-org/spegel/pull/581) Skip status response verification for containerd v2
## v0.0.24
### Added
- [#538](https://github.com/spegel-org/spegel/pull/538) Replace mock OCI client with in memory client.
- [#552](https://github.com/spegel-org/spegel/pull/552) Add support for VerticalPodAutoscaler in the Helm chart.
- [#556](https://github.com/spegel-org/spegel/pull/556) Add configuration for revisionHistoryLimit in the Helm Chart.
- [#573](https://github.com/spegel-org/spegel/pull/573) Use buffer pool for proxy copying data.
### Changed
- [#518](https://github.com/spegel-org/spegel/pull/518) Extend tests for image.
- [#519](https://github.com/spegel-org/spegel/pull/519) Extend tests for containerd.
- [#520](https://github.com/spegel-org/spegel/pull/520) Add tests for metrics.
- [#536](https://github.com/spegel-org/spegel/pull/536) Update Go version to 1.22.5.
- [#547](https://github.com/spegel-org/spegel/pull/547) Set blob content type to disable detection.
- [#553](https://github.com/spegel-org/spegel/pull/553) Re-use resources value for initContainer in the Helm Chart.
### Deprecated
### Removed
- [#517](https://github.com/spegel-org/spegel/pull/517) Remove deprecated CopyLayer function.
### Fixed
- [#535](https://github.com/spegel-org/spegel/pull/535) Fix Docker build casing checks.
### Security
## v0.0.23
### Added
- [#388](https://github.com/spegel-org/spegel/pull/388) Add support for deploying the Grafana dashboard with the Helm chart.
### Changed
- [#475](https://github.com/spegel-org/spegel/pull/475) Move resolving ref to digest to manifest handler.
- [#477](https://github.com/spegel-org/spegel/pull/477) Refactor distribution ref to simplify registry routing.
- [#479](https://github.com/spegel-org/spegel/pull/479) Enable goimports linter and fix errors.
- [#480](https://github.com/spegel-org/spegel/pull/480) Enable ireturn linter and fix errors.
- [#481](https://github.com/spegel-org/spegel/pull/481) Enable perfsprint linter and fix errors.
- [#482](https://github.com/spegel-org/spegel/pull/482) Enable gocritic linter and fix errors.
- [#483](https://github.com/spegel-org/spegel/pull/483) Update errcheck linter configuration and fix errors.
- [#487](https://github.com/spegel-org/spegel/pull/487) Move mirror metrics code to mirror handler.
- [#488](https://github.com/spegel-org/spegel/pull/488) Update existing registry errors and add more detail.
- [#495](https://github.com/spegel-org/spegel/pull/495) Modify e2e tests to allow reusing the same kind cluster.
- [#498](https://github.com/spegel-org/spegel/pull/498) Update to Go 1.22.
- [#499](https://github.com/spegel-org/spegel/pull/499) Add paralleltest linter and set all unit tests to run in parallel.
- [#501](https://github.com/spegel-org/spegel/pull/501) Rename mock router to memory router and add tests.
- [#507](https://github.com/spegel-org/spegel/pull/507) Change default resolve timeout to 20ms.
### Fixed
- [#460](https://github.com/spegel-org/spegel/pull/460) Fix environment variable for http-bootstrap-addr flag.
- [#471](https://github.com/spegel-org/spegel/pull/471) Fix handler key in request logging.
- [#490](https://github.com/spegel-org/spegel/pull/490) Close immediate channel after writing to it to close wait group in merge logic.
- [#491](https://github.com/spegel-org/spegel/pull/491) Fix so that resolve timeout does not cancel mirroring attempts.
- [#496](https://github.com/spegel-org/spegel/pull/496) Fix p2p bootstrap to run on failed readiness check.
## v0.0.22
### Added
- [#435](https://github.com/spegel-org/spegel/pull/435) Add pprof endpoints to enable profiling.
- [#434](https://github.com/spegel-org/spegel/pull/434) Add optional Containerd local content store to increase serve performance.
- [#438](https://github.com/spegel-org/spegel/pull/438) Set host path type for Containerd socket.
- [#449](https://github.com/spegel-org/spegel/pull/449) Replace zapr with slog and add log level configuration.
### Changed
- [#439](https://github.com/spegel-org/spegel/pull/439) Update Go version and fix toolchain version.
### Fixed
- [#452](https://github.com/spegel-org/spegel/pull/452) Fix Containerd Subscribe returning on any error.
### Security
- [#451](https://github.com/spegel-org/spegel/pull/451) Bump golang.org/x/net from 0.21.0 to 0.23.0.
## v0.0.21
### Added
### Added
- [#421](https://github.com/spegel-org/spegel/pull/421) Add conformance tests to e2e test.
- [#424](https://github.com/spegel-org/spegel/pull/424) Add option to append mirror configuration instead of overwriting.
@ -40,8 +312,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#431](https://github.com/spegel-org/spegel/pull/431) Fix import error caused by invalid file name.
## v0.0.20
### Added
### Added
- [#416](https://github.com/spegel-org/spegel/pull/416) Add image and Helm chart signing with Cosign.
@ -60,11 +332,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.0.19
> [!IMPORTANT]
> [!IMPORTANT]
> The Spegel repository has been moved from XenitAB to a new GitHub organization.
> Make sure to update the organization in the image and chart references.
### Added
### Added
- [#335](https://github.com/spegel-org/spegel/pull/335) Add k3s to compatibility guide.
- [#359](https://github.com/spegel-org/spegel/pull/359) Extend OCI client tests.
@ -102,7 +374,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.0.18
### Added
### Added
- [#331](https://github.com/spegel-org/spegel/pull/331) Document possible modifications required for k8s-digester.
- [#337](https://github.com/spegel-org/spegel/pull/337) Add HTTP bootstrapper.
@ -129,7 +401,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.0.17
### Added
### Added
- [#299](https://github.com/spegel-org/spegel/pull/299) Add update strategy configuration to Helm chart.
@ -152,7 +424,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.0.15
### Added
### Added
- [#270](https://github.com/spegel-org/spegel/pull/270) Add tests for local and external service port.
- [#262](https://github.com/spegel-org/spegel/pull/262) Enable misspell linter and fix spelling mistakes.
@ -169,7 +441,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.0.14
### Added
### Added
- [#237](https://github.com/spegel-org/spegel/pull/237) Verify discard unpacked layers setting.
@ -184,7 +456,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.0.13
### Added
### Added
- [#195](https://github.com/spegel-org/spegel/pull/195) Fix daemonset argument namespace to use helper-defined namespace value.
@ -205,7 +477,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.0.12
### Added
### Added
- [#182](https://github.com/spegel-org/spegel/pull/182) Add lscr.io as default registry.
@ -220,7 +492,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.0.11
### Added
### Added
- [#170](https://github.com/spegel-org/spegel/pull/170) Backup existing Containerd mirror configuration.
- [#171](https://github.com/spegel-org/spegel/pull/171) Add option to disable resolve.
@ -231,7 +503,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.0.10
### Added
### Added
- [#145](https://github.com/spegel-org/spegel/pull/145) Add new field to override Helm chart namespace.
- [#153](https://github.com/spegel-org/spegel/pull/153) Add option to disable resolving latest tags.
@ -265,7 +537,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.0.8
### Added
### Added
- [#125](https://github.com/spegel-org/spegel/pull/125) Add retry mirroring to new peer if current peer fails.
- [#127](https://github.com/spegel-org/spegel/pull/127) Add configuration for resolve retry and timeout.
@ -297,21 +569,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#42](https://github.com/spegel-org/spegel/pull/42) Only use bootstrap function for initial peer discovery.
- [#66](https://github.com/spegel-org/spegel/pull/66) Move mirror configuration logic to run as an init container.
### Fixed
- [#71](https://github.com/spegel-org/spegel/pull/71) Fix priority class name.
## v0.0.5
### Added
### Added
- [#29](https://github.com/spegel-org/spegel/pull/29) Make priority class name configurable and set a default value.
- [#49](https://github.com/spegel-org/spegel/pull/49) Add registry.k8s.io to registry mirror list.
- [#56](https://github.com/spegel-org/spegel/pull/56) Add gcr.io and k8s.gcr.io registries to default list.
### Changed
- [#32](https://github.com/spegel-org/spegel/pull/32) Update Go to 1.20.
- [#33](https://github.com/spegel-org/spegel/pull/33) Remove containerd info call when handling manifest request.
- [#48](https://github.com/spegel-org/spegel/pull/48) Replace multierr with stdlib errors join.
@ -332,7 +604,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.0.3
### Added
### Added
- [#18](https://github.com/spegel-org/spegel/pull/18) Add support to use Spegel instance on another node.

View File

@ -7,8 +7,12 @@ Thank you for considering contributing to Spegel, hopefully this document will m
The following tools are required to run the tests properly.
* go
* golangci-lint
* kind
* [golangci-lint](https://github.com/golangci/golangci-lint)
* [kind](https://github.com/kubernetes-sigs/kind)
* [goreleaser](https://github.com/goreleaser/goreleaser)
* [docker](https://docs.docker.com/get-started/get-docker/)
* [helm](https://github.com/helm/helm)
* [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl)
Run the linter and the unit tests to quickly validate changes.
@ -19,13 +23,13 @@ make lint test
Run the e2e tests which take a bit more time.
```shell
make e2e
make test-e2e
```
There are e2e tests for the different CNIs iptables, iptables-v6, and ipvs.
```shell
make e2e CNI=ipvs
make test-e2e E2E_CNI=ipvs
```
## Building
@ -33,16 +37,32 @@ make e2e CNI=ipvs
Build the Docker image locally.
```shell
make docker-build
make build-image
```
It is possible to specify a different image name and tag.
```shell
make docker-build IMG=exmaple.com/spegel TAG=feature
make build-image IMG=example.com/spegel TAG=feature
```
## Generating documentation
### Local debugging
Run the `dev-deploy` recipe which will create a Kind cluster with the proper configuration and deploy Spegel into it. If you run this command a second time the cluster will be kept but Spegel will be updated.
```shell
make dev-deploy
```
After the command has run you can get a kubeconfig file to access the cluster and do any debugging.
```shell
kind get kubeconfig --name spegel-dev > kubeconfig
export KUBECOONFIG=$(pwd)/kubeconfig
kubectl -n spegel get pods
```
## Generate Helm documentation
Changes to the Helm chart values will require the documentation to be regenerated.

View File

@ -1,16 +1,6 @@
FROM golang:1.21.7@sha256:549dd88a1a53715f177b41ab5fee25f7a376a6bb5322ac7abe263480d9554021 as builder
RUN mkdir /build
WORKDIR /build
COPY go.mod go.mod
COPY go.sum go.sum
RUN go mod download
COPY main.go main.go
COPY internal/ internal/
COPY pkg/ pkg/
RUN CGO_ENABLED=0 go build -installsuffix 'static' -o spegel .
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /build/spegel /app/
WORKDIR /app
ARG TARGETOS
ARG TARGETARCH
COPY ./dist/spegel_${TARGETOS}_${TARGETARCH}/spegel /
USER root:root
ENTRYPOINT ["./spegel"]
ENTRYPOINT ["/spegel"]

View File

@ -1,19 +1,29 @@
TAG = $$(git rev-parse --short HEAD)
IMG ?= ghcr.io/spegel-org/spegel:$(TAG)
CNI ?= iptables
IMG_NAME ?= ghcr.io/spegel-org/spegel
IMG_REF = $(IMG_NAME):$(TAG)
E2E_PROXY_MODE ?= iptables
E2E_IP_FAMILY ?= ipv4
lint:
golangci-lint run ./...
.PHONY: test
test:
build:
goreleaser build --snapshot --clean --single-target --skip before
build-image: build
docker build -t ${IMG_REF} .
test-unit:
go test ./...
docker-build:
docker build -t ${IMG} .
test-e2e: build-image
IMG_REF=${IMG_REF} \
E2E_PROXY_MODE=${E2E_PROXY_MODE} \
E2E_IP_FAMILY=${E2E_IP_FAMILY} \
go test ./test/e2e -v -timeout 200s -tags e2e -count 1 -run TestE2E
e2e: docker-build
./test/e2e/e2e.sh ${IMG} ${CNI}
dev-deploy: build-image
IMG_REF=${IMG_REF} go test ./test/e2e -v -timeout 200s -tags e2e -count 1 -run TestDevDeploy
tools:
GO111MODULE=on go install github.com/norwoodj/helm-docs/cmd/helm-docs

View File

@ -1,14 +1,17 @@
> [!NOTE]
> Weve started hosting community meetings every Tuesday at 17:00 CET. Find out how to participate at https://spegel.dev/project/community/#meeting.
# Spegel
Spegel, mirror in Swedish, is a stateless cluster local OCI registry mirror.
<p align="center">
<img src="./assets/overview.gif">
<img src="https://spegel.dev/images/overview.gif">
</p>
## Use Cases
## Features
Spegel is for you if you are looking to do any of the following:
Spegel is for you if you are looking to do any of the following.
* Locally cache images from external registries with no explicit configuration.
* Avoid cluster failure during external registry downtime.
@ -17,64 +20,13 @@ Spegel is for you if you are looking to do any of the following:
* Decrease egressing traffic outside of the clusters network.
* Increase image pull efficiency in edge node deployments.
## Background
## Getting Started
Kubernetes does a great job at distributing workloads on multiple nodes. Allowing node failures to occur without affecting uptime. A critical component for this to work is that each node has to be able to pull the workload images before they can start. Each replica running on a node will incur a pull operation. The images may be pulled from geographically close registries within the cloud provider, public registries, or self-hosted registries. This process has a flaw in that each node has to make this round trip separately. Why can't the nodes share the image among themselves?
Read the [getting started](https://spegel.dev/docs/getting-started/) guide to deploy Spegel.
Spegel enables each node in a Kubernetes cluster to act as a local registry mirror, allowing nodes to share images between themselves. Any image already pulled by a node will be available for any other node in the cluster to pull.
## Contributing
This has the benefit of reducing workload startup times and egress traffic as images will be stored locally within the cluster. On top of that it allows the scheduling of new workloads even when external registries are down.
## Installation
Before installing Spegel check the [compatibility guide](./docs/COMPATIBILITY.md) to make sure that it will work with your specific Kubernetes flavor. If everything checks out, the easiest method to deploy Spegel is with Helm.
```shell
helm upgrade --create-namespace --namespace spegel --install --version v0.0.21 spegel oci://ghcr.io/spegel-org/helm-charts/spegel
```
Refer to the [Helm Chart](./charts/spegel) for detailed configuration documentation.
## FAQ
Please consult the [FAQ](./docs/FAQ.md) if you run into any problems.
## Architecture
Spegel can run as a stateless application by exploiting the fact that an image pulled by a node is not immediately garbage collected. Spegel is deployed as a Daemonset on each node which acts as both the registry and mirror. Each instance is reachable both locally through a host port and a Service. This enables Containerd to be configured to use the localhost interface as a registry mirror and for Spegel instances to forward requests to each other.
<p align="center">
<img src="./assets/architecture.jpg">
</p>
Images are composed of multiple layers which are stored as individual files on the node disk. Each layer has a digest which is its identifier. Every node advertises the digests which are stored locally on disk. Kademlia is used to enable a distributed advertisement and lookup of digests. An image pull consists of multiple HTTP requests with one request per digest. The request is first sent to Spegel when an image is pulled if it is configured to act as the mirror for the registry. Spegel will lookup the digest within the cluster to see if any node has advertised that they have it. If a node is found the request will be forwarded to that Spegel instance which will serve the file with the specified digest. If a node is not found a 404 response will be returned and Containerd will fallback to using the actual remote registry.
In its core Spegel is a pull only OCI registry which runs locally on every Node in the Kubernetes cluster. Containerd is configured to use the local registry as a mirror, which would serve the image from within the cluster or from the source registry.
## Alternatives
### Private Registry
A common practice, especially for larger enterprises, is to run a private registry like Harbor to replicate images from public registries, storing them within the private network close to the cluster.
This is a great option for those who have the time and budget to invest in running and managing the infrastructure. For others, it may be a good practice but unattainable in reality.
Spegel does not aim to replace projects like [Harbor](https://github.com/goharbor/harbor) or [Zot](https://github.com/project-zot/zot) but instead complements them. Having a persistent copy of public images stored geographically close to a cluster is great. Spegel will however enable
nodes to pull from images closer as long as the images are somewhere within the cluster. Additionally, there is no guarantee that a self-managed private registry is always available. In these scenarios
running Spegel is like wearing both belt and suspenders.
### Dragonfly
[Dragonfly](https://github.com/dragonflyoss/Dragonfly2) is a great project that has been around for a while. In some aspects, Spegel takes inspiration from the work done by Dragonfly.
The difference is that Spegel aims to solve a smaller problem set. While it may mean fewer features it also means fewer moving components. Dragonfly requires both Redis and MySQL which
increases the resource consumption and burden on end users to manage additional resources. It also increases the risk of errors occurring during critical moments. The benefit of Spegel
is that it is stateless meaning that any temporary failure of nodes and communication should be easily resolved automatically.
### Kraken
[Kraken](https://github.com/uber/kraken) implements a similar solution to Spegel with its P2P agent component. It is however not heavily maintained, meaning that new features and security updates will not be added.
The problem set that Kraken is attempting to solve is however different from Spegel. It's focused on speeding up image distribution from registries serving thousands of large images. It does this by
having trackers and seeders distribute image layers through a BitTorrent-like method. This means that Kraken requires more moving components to function. Kraken also does not support using it
as a transparent pull-through mirror. Meaning that any image that is supposed to be pulled through Kraken will require changing the registry URL in the image name. This has to be done for all
Pods in the cluster.
Read [contribution guidelines](./CONTRIBUTING.md) for instructions on how to build and test Spegel.
## Acknowledgements

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

View File

@ -2,61 +2,21 @@
Stateless cluster local OCI registry mirror.
## Installation
Make sure that you have read the [compatibility guide](../../docs/COMPATIBILITY.md) before proceeding the with the installation.
### CLI
Delpoy Spegel with the Helm CLI.
```sh
helm upgrade --create-namespace --namespace spegel --install --version v0.0.21 spegel oci://ghcr.io/spegel-org/helm-charts/spegel
```
### Flux
Deploy Spegel with Flux.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: spegel
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: spegel
namespace: spegel
spec:
type: "oci"
interval: 5m0s
url: oci://ghcr.io/spegel-org/helm-charts
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: spegel
namespace: spegel
spec:
interval: 1m
chart:
spec:
chart: spegel
version: "v0.0.21"
interval: 5m
sourceRef:
kind: HelmRepository
name: spegel
```
Read the [getting started](https://spegel.dev/docs/getting-started/) guide to deploy Spegel.
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | Affinity settings for pod assignment. |
| basicAuthSecretName | string | `""` | Name of secret containing basic authentication credentials for registry. |
| clusterDomain | string | `"cluster.local."` | Domain configured for service domain names. |
| commonLabels | object | `{}` | Common labels to apply to all rendered resources. |
| fullnameOverride | string | `""` | Overrides the full name of the chart. |
| grafanaDashboard.annotations | object | `{}` | Annotations that ConfigMaps can have to get configured in Grafana, See: sidecar.dashboards.folderAnnotation for specifying the dashboard folder. https://github.com/grafana/helm-charts/tree/main/charts/grafana |
| grafanaDashboard.enabled | bool | `false` | If true creates a Grafana dashboard. |
| grafanaDashboard.sidecarLabel | string | `"grafana_dashboard"` | Label that ConfigMaps should have to be loaded as dashboards. |
| grafanaDashboard.sidecarLabelValue | string | `"1"` | Label value that ConfigMaps should have to be loaded as dashboards. |
| image.digest | string | `""` | Image digest. |
| image.pullPolicy | string | `"IfNotPresent"` | Image Pull Policy. |
| image.repository | string | `"ghcr.io/spegel-org/spegel"` | Image repository. |
@ -68,10 +28,13 @@ spec:
| podAnnotations | object | `{}` | Annotations to add to the pod. |
| podSecurityContext | object | `{}` | Security context for the pod. |
| priorityClassName | string | `"system-node-critical"` | Priority class name to use for the pod. |
| resources | object | `{}` | Resource requests and limits for the Spegel container. |
| securityContext | object | `{}` | Security context for the Spegel container. |
| resources | object | `{"limits":{"memory":"128Mi"},"requests":{"memory":"128Mi"}}` | Resource requests and limits for the Spegel container. |
| revisionHistoryLimit | int | `10` | The number of old history to retain to allow rollback. |
| securityContext | object | `{"readOnlyRootFilesystem":true}` | Security context for the Spegel container. |
| service.cleanup.port | int | `8080` | Port to expose cleanup probe on. |
| service.metrics.port | int | `9090` | Port to expose the metrics via the service. |
| service.registry.hostPort | int | `30020` | Local host port to expose the registry. |
| service.registry.nodeIp | string | `""` | Override the NODE_ID environment variable. It defaults to the field status.hostIP |
| service.registry.nodePort | int | `30021` | Node port to expose the registry via the service. |
| service.registry.port | int | `5000` | Port to expose the registry via the service. |
| service.registry.topologyAwareHintsEnabled | bool | `true` | If true adds topology aware hints annotation to node port service. |
@ -81,20 +44,30 @@ spec:
| serviceMonitor.enabled | bool | `false` | If true creates a Prometheus Service Monitor. |
| serviceMonitor.interval | string | `"60s"` | Prometheus scrape interval. |
| serviceMonitor.labels | object | `{}` | Service monitor specific labels for prometheus to discover servicemonitor. |
| serviceMonitor.metricRelabelings | list | `[]` | List of relabeling rules to apply to the samples before ingestion. |
| serviceMonitor.relabelings | list | `[]` | List of relabeling rules to apply the targets metadata labels. |
| serviceMonitor.scrapeTimeout | string | `"30s"` | Prometheus scrape interval timeout. |
| spegel.additionalMirrorRegistries | list | `[]` | Additional target mirror registries other than Spegel. |
| spegel.appendMirrors | bool | `false` | When true existing mirror configuration will be appended to instead of replaced. |
| spegel.blobSpeed | string | `""` | Maximum write speed per request when serving blob layers. Should be an integer followed by unit Bps, KBps, MBps, GBps, or TBps. |
| spegel.additionalMirrorTargets | list | `[]` | Additional target mirror registries other than Spegel. |
| spegel.containerdContentPath | string | `"/var/lib/containerd/io.containerd.content.v1.content"` | Path to Containerd content store.. |
| spegel.containerdMirrorAdd | bool | `true` | If true Spegel will add mirror configuration to the node. |
| spegel.containerdNamespace | string | `"k8s.io"` | Containerd namespace where images are stored. |
| spegel.containerdRegistryConfigPath | string | `"/etc/containerd/certs.d"` | Path to Containerd mirror configuration. |
| spegel.containerdSock | string | `"/run/containerd/containerd.sock"` | Path to Containerd socket. |
| spegel.kubeconfigPath | string | `""` | Path to Kubeconfig credentials, should only be set if Spegel is run in an environment without RBAC. |
| spegel.mirrorResolveRetries | int | `3` | Max ammount of mirrors to attempt. |
| spegel.mirrorResolveTimeout | string | `"5s"` | Max duration spent finding a mirror. |
| spegel.registries | list | `["https://cgr.dev","https://docker.io","https://ghcr.io","https://quay.io","https://mcr.microsoft.com","https://public.ecr.aws","https://gcr.io","https://registry.k8s.io","https://k8s.gcr.io","https://lscr.io"]` | Registries for which mirror configuration will be created. |
| spegel.debugWebEnabled | bool | `false` | When true enables debug web page. |
| spegel.logLevel | string | `"INFO"` | Minimum log level to output. Value should be DEBUG, INFO, WARN, or ERROR. |
| spegel.mirrorResolveRetries | int | `3` | Max amount of mirrors to attempt. |
| spegel.mirrorResolveTimeout | string | `"20ms"` | Max duration spent finding a mirror. |
| spegel.mirroredRegistries | list | `[]` | Registries for which mirror configuration will be created. Empty means all registires will be mirrored. |
| spegel.prependExisting | bool | `false` | When true existing mirror configuration will be kept and Spegel will prepend it's configuration. |
| spegel.resolveLatestTag | bool | `true` | When true latest tags will be resolved to digests. |
| spegel.resolveTags | bool | `true` | When true Spegel will resolve tags to digests. |
| tolerations | list | `[{"key":"CriticalAddonsOnly","operator":"Exists"},{"effect":"NoExecute","operator":"Exists"},{"effect":"NoSchedule","operator":"Exists"}]` | Tolerations for pod assignment. |
| updateStrategy | object | `{}` | An update strategy to replace existing pods with new pods. |
| updateStrategy | object | `{}` | An update strategy to replace existing pods with new pods. |
| verticalPodAutoscaler.controlledResources | list | `[]` | List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory |
| verticalPodAutoscaler.controlledValues | string | `"RequestsAndLimits"` | Specifies which resource values should be controlled: RequestsOnly or RequestsAndLimits. |
| verticalPodAutoscaler.enabled | bool | `false` | If true creates a Vertical Pod Autoscaler. |
| verticalPodAutoscaler.maxAllowed | object | `{}` | Define the max allowed resources for the pod |
| verticalPodAutoscaler.minAllowed | object | `{}` | Define the min allowed resources for the pod |
| verticalPodAutoscaler.recommenders | list | `[]` | Recommender responsible for generating recommendation for the object. List should be empty (then the default recommender will generate the recommendation) or contain exactly one recommender. |
| verticalPodAutoscaler.updatePolicy.minReplicas | int | `2` | Specifies minimal number of replicas which need to be alive for VPA Updater to attempt pod eviction |
| verticalPodAutoscaler.updatePolicy.updateMode | string | `"Auto"` | Specifies whether recommended updates are applied when a Pod is started and whether recommended updates are applied during the life of a Pod. Possible values are "Off", "Initial", "Recreate", and "Auto". |

View File

@ -2,54 +2,6 @@
{{ template "chart.description" . }}
## Installation
Read the [getting started](https://spegel.dev/docs/getting-started/) guide to deploy Spegel.
Make sure that you have read the [compatibility guide](../../docs/COMPATIBILITY.md) before proceeding the with the installation.
### CLI
Delpoy Spegel with the Helm CLI.
```sh
helm upgrade --create-namespace --namespace spegel --install --version v0.0.21 spegel oci://ghcr.io/spegel-org/helm-charts/spegel
```
### Flux
Deploy Spegel with Flux.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: spegel
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: spegel
namespace: spegel
spec:
type: "oci"
interval: 5m0s
url: oci://ghcr.io/spegel-org/helm-charts
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: spegel
namespace: spegel
spec:
interval: 1m
chart:
spec:
chart: spegel
version: "v0.0.21"
interval: 5m
sourceRef:
kind: HelmRepository
name: spegel
```
{{ template "chart.valuesSection" . }}
{{ template "chart.valuesSection" . }}

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@ If release name contains chart name it will be used as a full name.
{{- end }}
{{/*
Creates the namespace for the chart.
Creates the namespace for the chart.
Defaults to the Release namespace unless the namespaceOverride is defined.
*/}}
{{- define "spegel.namespace" -}}
@ -53,6 +53,13 @@ helm.sh/chart: {{ include "spegel.chart" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- with .Values.commonLabels }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{/*
{{- end }}
{{- end }}
{{/*
@ -80,3 +87,16 @@ Image reference
{{- .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}
{{- end }}
{{- end }}
{{/*
Host networking
*/}}
{{- define "networking.nodeIp" -}}
{{- if .Values.service.registry.nodeIp -}}
value: {{ .Values.service.registry.nodeIp }}
{{- else -}}
valueFrom:
fieldRef:
fieldPath: status.hostIP
{{- end -}}
{{- end -}}

View File

@ -6,6 +6,7 @@ metadata:
labels:
{{- include "spegel.labels" . | nindent 4 }}
spec:
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
updateStrategy:
{{- toYaml .Values.updateStrategy | nindent 4 }}
selector:
@ -19,6 +20,9 @@ spec:
{{- end }}
labels:
{{- include "spegel.selectorLabels" . | nindent 8 }}
{{- with .Values.commonLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
@ -37,31 +41,37 @@ spec:
{{- toYaml .Values.securityContext | nindent 12 }}
args:
- configuration
- --log-level={{ .Values.spegel.logLevel }}
- --containerd-registry-config-path={{ .Values.spegel.containerdRegistryConfigPath }}
{{- with .Values.spegel.registries }}
- --registries
{{- with .Values.spegel.mirroredRegistries }}
- --mirrored-registries
{{- range . }}
- {{ . | quote }}
{{- end }}
{{- end }}
- --mirror-registries
- --mirror-targets
- http://$(NODE_IP):{{ .Values.service.registry.hostPort }}
- http://$(NODE_IP):{{ .Values.service.registry.nodePort }}
{{- with .Values.spegel.additionalMirrorRegistries }}
{{- with .Values.spegel.additionalMirrorTargets }}
{{- range . }}
- {{ . | quote }}
{{- end }}
{{- end }}
- --resolve-tags={{ .Values.spegel.resolveTags }}
- --append-mirrors={{ .Values.spegel.appendMirrors }}
- --prepend-existing={{ .Values.spegel.prependExisting }}
env:
- name: NODE_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
{{- include "networking.nodeIp" . | nindent 10 }}
resources:
{{- toYaml .Values.resources | nindent 10 }}
volumeMounts:
- name: containerd-config
mountPath: {{ .Values.spegel.containerdRegistryConfigPath }}
{{- if .Values.basicAuthSecretName }}
- name: basic-auth
mountPath: "/etc/secrets/basic-auth"
readOnly: true
{{- end }}
{{- end }}
containers:
- name: registry
@ -71,13 +81,14 @@ spec:
{{- toYaml .Values.securityContext | nindent 12 }}
args:
- registry
- --log-level={{ .Values.spegel.logLevel }}
- --mirror-resolve-retries={{ .Values.spegel.mirrorResolveRetries }}
- --mirror-resolve-timeout={{ .Values.spegel.mirrorResolveTimeout }}
- --registry-addr=:{{ .Values.service.registry.port }}
- --router-addr=:{{ .Values.service.router.port }}
- --metrics-addr=:{{ .Values.service.metrics.port }}
{{- with .Values.spegel.registries }}
- --registries
{{- with .Values.spegel.mirroredRegistries }}
- --mirrored-registries
{{- range . }}
- {{ . | quote }}
{{- end }}
@ -85,25 +96,32 @@ spec:
- --containerd-sock={{ .Values.spegel.containerdSock }}
- --containerd-namespace={{ .Values.spegel.containerdNamespace }}
- --containerd-registry-config-path={{ .Values.spegel.containerdRegistryConfigPath }}
- --bootstrap-kind=kubernetes
{{- with .Values.spegel.kubeconfigPath }}
- --kubeconfig-path={{ . }}
{{- end }}
- --leader-election-namespace={{ include "spegel.namespace" . }}
- --leader-election-name={{ .Release.Name }}-leader-election
- --bootstrap-kind=dns
- --dns-bootstrap-domain={{ include "spegel.fullname" . }}-bootstrap.{{ include "spegel.namespace" . }}.svc.{{ .Values.clusterDomain }}
- --resolve-latest-tag={{ .Values.spegel.resolveLatestTag }}
- --local-addr=$(NODE_IP):{{ .Values.service.registry.hostPort }}
{{- with .Values.spegel.blobSpeed }}
- --blob-speed={{ . }}
{{- end }}
{{- with .Values.spegel.containerdContentPath }}
- --containerd-content-path={{ . }}
{{- end }}
- --debug-web-enabled={{ .Values.spegel.debugWebEnabled }}
env:
- name: NODE_IP
- name: DATA_DIR
value: ""
{{- if ((.Values.resources).limits).cpu }}
- name: GOMAXPROCS
valueFrom:
fieldRef:
fieldPath: status.hostIP
resourceFieldRef:
resource: limits.cpu
divisor: 1
{{- end }}
{{- if ((.Values.resources).limits).memory }}
- name: GOMEMLIMIT
valueFrom:
resourceFieldRef:
resource: limits.memory
divisor: 1
{{- end }}
- name: NODE_IP
{{- include "networking.nodeIp" . | nindent 10 }}
ports:
- name: registry
containerPort: {{ .Values.service.registry.port }}
@ -118,7 +136,7 @@ spec:
# Startup may take a bit longer on bootsrap as Pods need to find each other.
# This is why the startup proben is a bit more forgiving, while hitting the endpoint more often.
startupProbe:
periodSeconds: 1
periodSeconds: 3
failureThreshold: 60
httpGet:
path: /healthz
@ -128,6 +146,11 @@ spec:
path: /healthz
port: registry
volumeMounts:
{{- if .Values.basicAuthSecretName }}
- name: basic-auth
mountPath: "/etc/secrets/basic-auth"
readOnly: true
{{- end }}
- name: containerd-sock
mountPath: {{ .Values.spegel.containerdSock }}
{{- with .Values.spegel.containerdContentPath }}
@ -138,9 +161,15 @@ spec:
resources:
{{- toYaml .Values.resources | nindent 10 }}
volumes:
{{- with .Values.basicAuthSecretName }}
- name: basic-auth
secret:
secretName: {{ . }}
{{- end }}
- name: containerd-sock
hostPath:
path: {{ .Values.spegel.containerdSock }}
type: Socket
{{- with .Values.spegel.containerdContentPath }}
- name: containerd-content
hostPath:

View File

@ -0,0 +1,17 @@
{{- if .Values.grafanaDashboard.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "spegel.fullname" . }}-dashboard
namespace: {{ include "spegel.namespace" . }}
labels:
{{ .Values.grafanaDashboard.sidecarLabel }}: {{ .Values.grafanaDashboard.sidecarLabelValue | quote }}
{{- include "spegel.labels" . | nindent 4 }}
{{- with .Values.grafanaDashboard.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
data:
spegel.json: |-
{{ .Files.Get "monitoring/grafana-dashboard.json" | indent 6 }}
{{- end }}

View File

@ -0,0 +1,106 @@
{{- if .Values.spegel.containerdMirrorAdd }}
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: {{ include "spegel.fullname" . }}-cleanup
namespace: {{ include "spegel.namespace" . }}
labels:
app.kubernetes.io/component: cleanup
{{- include "spegel.labels" . | nindent 4 }}
annotations:
helm.sh/hook: "post-delete"
helm.sh/hook-delete-policy: "before-hook-creation, hook-succeeded"
helm.sh/hook-weight: "0"
spec:
selector:
matchLabels:
app.kubernetes.io/component: cleanup
{{- include "spegel.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
app.kubernetes.io/component: cleanup
{{- include "spegel.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
priorityClassName: {{ .Values.priorityClassName }}
containers:
- name: cleanup
image: "{{ include "spegel.image" . }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- cleanup
- --containerd-registry-config-path={{ .Values.spegel.containerdRegistryConfigPath }}
- --addr=:{{ .Values.service.cleanup.port }}
readinessProbe:
httpGet:
path: /healthz
port: readiness
ports:
- name: readiness
containerPort: {{ .Values.service.cleanup.port }}
protocol: TCP
volumeMounts:
- name: containerd-config
mountPath: {{ .Values.spegel.containerdRegistryConfigPath }}
volumes:
- name: containerd-config
hostPath:
path: {{ .Values.spegel.containerdRegistryConfigPath }}
type: DirectoryOrCreate
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "spegel.fullname" . }}-cleanup
namespace: {{ include "spegel.namespace" . }}
labels:
app.kubernetes.io/component: cleanup
{{- include "spegel.labels" . | nindent 4 }}
annotations:
helm.sh/hook: "post-delete"
helm.sh/hook-delete-policy: "before-hook-creation, hook-succeeded"
helm.sh/hook-weight: "0"
spec:
selector:
app.kubernetes.io/component: cleanup
{{- include "spegel.selectorLabels" . | nindent 4 }}
clusterIP: None
publishNotReadyAddresses: true
ports:
- name: readiness
port: {{ .Values.service.cleanup.port }}
protocol: TCP
---
apiVersion: v1
kind: Pod
metadata:
name: {{ include "spegel.fullname" . }}-cleanup-wait
namespace: {{ include "spegel.namespace" . }}
labels:
app.kubernetes.io/component: cleanup-wait
{{- include "spegel.labels" . | nindent 4 }}
annotations:
helm.sh/hook: "post-delete"
helm.sh/hook-delete-policy: "before-hook-creation, hook-succeeded"
helm.sh/hook-weight: "1"
spec:
containers:
- name: cleanup-wait
image: "{{ include "spegel.image" . }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- cleanup-wait
- --probe-endpoint={{ include "spegel.fullname" . }}-cleanup.{{ include "spegel.namespace" . }}.svc.{{ .Values.clusterDomain }}:{{ .Values.service.cleanup.port }}
restartPolicy: Never
terminationGracePeriodSeconds: 0
{{- end }}

View File

@ -9,31 +9,3 @@ metadata:
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "spegel.fullname" . }}
namespace: {{ include "spegel.namespace" . }}
labels:
{{- include "spegel.labels" . | nindent 4 }}
rules:
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "list", "watch", "create", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "spegel.fullname" . }}
namespace: {{ include "spegel.namespace" . }}
labels:
{{- include "spegel.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "spegel.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "spegel.serviceAccountName" . }}
namespace: {{ include "spegel.namespace" . }}

View File

@ -24,7 +24,7 @@ metadata:
{{- include "spegel.labels" . | nindent 4 }}
{{- if .Values.service.registry.topologyAwareHintsEnabled }}
annotations:
service.kubernetes.io/topology-aware-hints: auto
service.kubernetes.io/topology-mode: "auto"
{{- end }}
spec:
type: NodePort
@ -36,3 +36,20 @@ spec:
targetPort: registry
nodePort: {{ .Values.service.registry.nodePort }}
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "spegel.fullname" . }}-bootstrap
namespace: {{ include "spegel.namespace" . }}
labels:
{{- include "spegel.labels" . | nindent 4 }}
spec:
selector:
{{- include "spegel.selectorLabels" . | nindent 4 }}
clusterIP: None
publishNotReadyAddresses: true
ports:
- name: router
port: {{ .Values.service.router.port }}
protocol: TCP

View File

@ -18,4 +18,12 @@ spec:
- port: metrics
interval: {{ .Values.serviceMonitor.interval }}
scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }}
{{- with .Values.serviceMonitor.relabelings }}
relabelings:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.serviceMonitor.metricRelabelings }}
metricRelabelings:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,40 @@
{{- if and (.Capabilities.APIVersions.Has "autoscaling.k8s.io/v1") (.Values.verticalPodAutoscaler.enabled) }}
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: {{ include "spegel.fullname" . }}
namespace: {{ include "spegel.namespace" . }}
labels:
{{- include "spegel.labels" . | nindent 4 }}
spec:
{{- with .Values.verticalPodAutoscaler.recommenders }}
recommenders:
{{- toYaml . | nindent 4 }}
{{- end }}
resourcePolicy:
containerPolicies:
- containerName: registry
{{- with .Values.verticalPodAutoscaler.controlledResources }}
controlledResources:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.verticalPodAutoscaler.controlledValues }}
controlledValues: {{ .Values.verticalPodAutoscaler.controlledValues }}
{{- end }}
{{- if .Values.verticalPodAutoscaler.maxAllowed }}
maxAllowed:
{{- toYaml .Values.verticalPodAutoscaler.maxAllowed | nindent 8 }}
{{- end }}
{{- if .Values.verticalPodAutoscaler.minAllowed }}
minAllowed:
{{- toYaml .Values.verticalPodAutoscaler.minAllowed | nindent 8 }}
{{- end }}
targetRef:
apiVersion: apps/v1
kind: DaemonSet
name: {{ include "spegel.fullname" . }}
{{- with .Values.verticalPodAutoscaler.updatePolicy }}
updatePolicy:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@ -8,7 +8,6 @@ image:
# -- Image digest.
digest: ""
# -- Image Pull Secrets
imagePullSecrets: []
# -- Overrides the name of the chart.
@ -32,17 +31,17 @@ podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
# -- The number of old history to retain to allow rollback.
revisionHistoryLimit: 10
# -- Security context for the Spegel container.
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
securityContext:
readOnlyRootFilesystem: true
service:
registry:
# -- Override the NODE_ID environment variable. It defaults to the field status.hostIP
nodeIp: ""
# -- Port to expose the registry via the service.
port: 5000
# -- Node port to expose the registry via the service.
@ -57,19 +56,16 @@ service:
metrics:
# -- Port to expose the metrics via the service.
port: 9090
cleanup:
# -- Port to expose cleanup probe on.
port: 8080
# -- Resource requests and limits for the Spegel container.
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
resources:
requests:
memory: 128Mi
limits:
memory: 128Mi
# -- Node selector for pod assignment.
nodeSelector:
@ -94,6 +90,12 @@ tolerations:
# -- Affinity settings for pod assignment.
affinity: {}
# -- Common labels to apply to all rendered resources.
commonLabels: {}
# -- Domain configured for service domain names.
clusterDomain: cluster.local.
serviceMonitor:
# -- If true creates a Prometheus Service Monitor.
enabled: false
@ -103,29 +105,42 @@ serviceMonitor:
scrapeTimeout: 30s
# -- Service monitor specific labels for prometheus to discover servicemonitor.
labels: {}
# -- List of relabeling rules to apply the targets metadata labels.
relabelings: []
# -- List of relabeling rules to apply to the samples before ingestion.
metricRelabelings: []
grafanaDashboard:
# -- If true creates a Grafana dashboard.
enabled: false
# -- Label that ConfigMaps should have to be loaded as dashboards.
sidecarLabel: "grafana_dashboard"
# -- Label value that ConfigMaps should have to be loaded as dashboards.
sidecarLabelValue: "1"
# -- Annotations that ConfigMaps can have to get configured in Grafana,
# See: sidecar.dashboards.folderAnnotation for specifying the dashboard folder.
# https://github.com/grafana/helm-charts/tree/main/charts/grafana
annotations: {}
# -- Priority class name to use for the pod.
priorityClassName: system-node-critical
# -- Name of secret containing basic authentication credentials for registry.
basicAuthSecretName: ""
spegel:
# -- Registries for which mirror configuration will be created.
registries:
- https://cgr.dev
- https://docker.io
- https://ghcr.io
- https://quay.io
- https://mcr.microsoft.com
- https://public.ecr.aws
- https://gcr.io
- https://registry.k8s.io
- https://k8s.gcr.io
- https://lscr.io
# -- Minimum log level to output. Value should be DEBUG, INFO, WARN, or ERROR.
logLevel: "INFO"
# -- Registries for which mirror configuration will be created. Empty means all registires will be mirrored.
mirroredRegistries: []
# - https://docker.io
# - https://ghcr.io
# -- Additional target mirror registries other than Spegel.
additionalMirrorRegistries: []
# -- Max ammount of mirrors to attempt.
additionalMirrorTargets: []
# -- Max amount of mirrors to attempt.
mirrorResolveRetries: 3
# -- Max duration spent finding a mirror.
mirrorResolveTimeout: "5s"
mirrorResolveTimeout: "20ms"
# -- Path to Containerd socket.
containerdSock: "/run/containerd/containerd.sock"
# -- Containerd namespace where images are stored.
@ -136,13 +151,43 @@ spegel:
containerdContentPath: "/var/lib/containerd/io.containerd.content.v1.content"
# -- If true Spegel will add mirror configuration to the node.
containerdMirrorAdd: true
# -- Path to Kubeconfig credentials, should only be set if Spegel is run in an environment without RBAC.
kubeconfigPath: ""
# -- When true Spegel will resolve tags to digests.
resolveTags: true
# -- When true latest tags will be resolved to digests.
resolveLatestTag: true
# -- Maximum write speed per request when serving blob layers. Should be an integer followed by unit Bps, KBps, MBps, GBps, or TBps.
blobSpeed: ""
# -- When true existing mirror configuration will be appended to instead of replaced.
appendMirrors: false
# -- When true existing mirror configuration will be kept and Spegel will prepend it's configuration.
prependExisting: false
# -- When true enables debug web page.
debugWebEnabled: false
verticalPodAutoscaler:
# -- If true creates a Vertical Pod Autoscaler.
enabled: false
# -- Recommender responsible for generating recommendation for the object.
# List should be empty (then the default recommender will generate the recommendation)
# or contain exactly one recommender.
recommenders: []
# - name: custom-recommender-performance
# -- List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory
controlledResources: []
# -- Specifies which resource values should be controlled: RequestsOnly or RequestsAndLimits.
controlledValues: RequestsAndLimits
# -- Define the max allowed resources for the pod
maxAllowed: {}
# cpu: 100m
# memory: 128Mi
# -- Define the min allowed resources for the pod
minAllowed: {}
# cpu: 100m
# memory: 128Mi
updatePolicy:
# -- Specifies minimal number of replicas which need to be alive for VPA Updater to attempt pod eviction
minReplicas: 2
# -- Specifies whether recommended updates are applied when a Pod is started and whether recommended updates
# are applied during the life of a Pod. Possible values are "Off", "Initial", "Recreate", and "Auto".
updateMode: Auto

View File

@ -1,77 +0,0 @@
# Compatibility
Currently, Spegel only works with Containerd, in the future other container runtime interfaces may be supported. Spegel relies on [Containerd registry mirroring](https://github.com/containerd/containerd/blob/main/docs/hosts.md#cri) to route requests to the correct destination.
This requires Containerd to be properly configured, if it is not Spegel will exit. First of all the registry config path needs to be set, this is not done by default in Containerd. Second of all discarding unpacked layers cannot be enabled.
Some Kubernetes flavors come with this setting out of the box, while others do not. Spegel is not able to write this configuration for you as it requires a restart of Containerd to take effect.
```toml
version = 2
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
[plugins."io.containerd.grpc.v1.cri".containerd]
discard_unpacked_layers = false
```
# Kubernetes
Spegel has been tested on the following Kubernetes distributions for compatibility. Green status means Spegel will work out of the box, yellow will require additional configuration, and red means that Spegel will not work.
| Status | Distribution |
| --- | --- |
| :green_circle: | AKS |
| :green_circle: | Minikube |
| :yellow_circle: | EKS |
| :yellow_circle: | K3S |
| :yellow_circle: | Talos |
| :red_circle: | GKE |
| :red_circle: | DigitalOcean |
## EKS
Discard unpacked layers is enabled by default, meaning that layers that are not required for the container runtime will be removed after consumed.
This needs to be disabled as otherwise all of the required layers of an image would not be present on the node.
The best way to change Containerd settings in EKS is to add the configuration to the import directory using a custom node bootstrap script.
```shell
#!/bin/bash
set -ex
mkdir -p /etc/containerd/config.d
cat > /etc/containerd/config.d/spegel.toml << EOL
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
[plugins."io.containerd.grpc.v1.cri".containerd]
discard_unpacked_layers = false
EOL
/etc/eks/bootstrap.sh
```
## K3S
K3S embeds Spegel, refer to their [documentation](https://docs.k3s.io/installation/registry-mirror?_highlight=spegel) for deployment information.
## Talos
Talos comes with Pod Security Admission [pre-configured](https://www.talos.dev/latest/kubernetes-guides/configuration/pod-security/). The default profile is too restrictive and needs to be changed to privileged.
```shell
kubectl label namespace spegel pod-security.kubernetes.io/enforce=privileged
```
Talos also uses a different path as its Containerd registry config path.
```yaml
spegel:
containerdRegistryConfigPath: /etc/cri/conf.d/hosts
```
## GKE
GKE does not set the registry config path in its Containerd configuration. On top of that it uses the old mirror configuration for the internal mirroring service.
## DigitalOcean
DigitalOcean does not set the registry config path in its Containerd configuration.

View File

@ -1,114 +0,0 @@
# FAQ
Frequently asked questions, please read these before creating a new issue.
## Can I use Spegel in production?
Spegel is being used by multiple users in production for over a year without any major issues. The great thing is that pulling images would not stop working if you for some reason would find an issue with Spegel.
A fallback to the original registry will always occur if Spegel can't be reached or serve the requested image.
## How do I know that Spegel is working?
Spegel is meant to be a painless experience to install, meaning that it may be difficult initially to know if things are working or not. Simply put a good indicator that things are working is if all Spegel pods have started and are in a ready state.
Spegel does a couple of checks on startup to verify that any required configuration is correct, if it is not it will exit with an error. While it runs it will log all received requests, both those it mirrors and it serves.
An incoming request to Spegel that is mirrored will receive the following log.
```
{"level":"info","ts":1692304805.9038486,"caller":"gin@v0.0.9/logger.go:53","msg":"","path":"/v2/library/nginx/blobs/sha256:1cb127bd932119089b5ffb612ffa84537ddd1318e6784f2fce80916bbb8bd166","status":200,"method":"GET","latency":0.005075836,"ip":"172.18.0.5","handler":"mirror"}
```
While the Spegel instance on the other end will log.
```
{"level":"info","ts":1692304805.9035861,"caller":"gin@v0.0.9/logger.go:53","msg":"","path":"/v2/library/nginx/blobs/sha256:1cb127bd932119089b5ffb612ffa84537ddd1318e6784f2fce80916bbb8bd166","status":200,"method":"GET","latency":0.003644997,"ip":"172.18.0.5","handler":"blob"}
```
## Why am I not able to pull the new version of my tagged image?
Reusing the same tag multiple times for different versions of an image is generally a bad idea. The most common scenario is the use of the `latest` tag. This makes it difficult to determine which version of the image is being used. On top of that, the image will not be updated if it is already cached on the node.
Some people have chosen to power forward with reusing tags and chosen to instead set the image pull policy to `AlwaysPull`, forcing the image to be updated every time a pod is started. This will however not work with Spegel as the tag could be resolved by another node in the cluster resulting in the same "old" image being pulled.
There are two solutions to work around this problem, allowing users to continue with their way of working before using Spegel.
The best and preferable solution is to deploy [k8s-digester](https://github.com/google/k8s-digester) alongside Spegel. This will allow you to enjoy all the benefits of Spegel will continuously updating image tag versions. The way it works is that k8s-digester will, for each pod created, resolve tags to image digests and add them to the image reference.
This means that all pods that originally reference images by tag will instead do so with digest. This means that k8s-digester will resolve the new digest for a tag if a new version is pushed to the registry. Using k8s-digester means that tags will be updated while using Spegel to distribute the layers between nodes. It also means that Spegel will be able
to continue distributing images if the external registry became unavailable. The reason this works is that the mutating webhook is configured to ignore errors, and instead, Spegel will be used to resolve the tag to a digest.
One caveat when deploying k8s-digester is that it will by default modify both pods but also any other parent resource that creates pods. This in turn has the side effect of only setting the
digest once when the parent resource is created, and never again. For that reason it is a good idea to modify the mutating webhook to only include pods, that way the digest will be
updated every time a new pod is created.
```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: digester-mutating-webhook-configuration
labels:
control-plane: controller-manager
digester/operation: webhook
digester/system: "yes"
webhooks:
- name: digester-webhook-service.digester-system.svc
admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: digester-webhook-service
namespace: digester-system
path: /v1/mutate
caBundle: Cg==
failurePolicy: Ignore # kpt-set: ${failure-policy}
namespaceSelector:
matchLabels:
digest-resolution: enabled
reinvocationPolicy: IfNeeded
rules:
- resources:
- pods
apiGroups:
- ''
apiVersions:
- v1
operations:
- CREATE
- UPDATE
scope: Namespaced
sideEffects: None
timeoutSeconds: 15
```
The second option, which should be used only if using k8s-digester is not possible, is to disable tag resolving altogether in Spegel. There are two options when doing this. It can either be disabled only for `latest` tags or for all tags.
This can be done by changing the Helm charts values from their defaults.
```yaml
spegel:
resolveTags: false
resolveLatestTag: false
```
Please note that this does however remove Spegel's ability to protect against registry outages for any images referenced by tags.
## Why am I able to pull private images without image pull secrets?
An image pulled by a Kubernetes node is cached locally on disk. Meaning that other pods running on the same node that require the same image do not have to pull the same image again. Spegel relies on this mechanism to be able to distribute images.
This may however not be a desirable feature when running a multi-tenant cluster where private images are pulled using credentials. In this scenario, only those pods with the correct credentials would be able to use the image.
Ownership of private images has been an issue for a long time in Kubernetes as indicated by the unresolved issue https://github.com/kubernetes/kubernetes/issues/18787 created back in 2015. The short answer is that a good solution does not exist, with or without Spegel.
The current [suggested solution](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#alwayspullimages) is to enforce an `AlwaysPull` image policy for private images that require authentication. Doing so will force a request to the registry to
validate the digest or resolve the tag. This request will only succeed with the proper authentication. This is a mediocre solution at best as it creates a hard dependency on the external registry, meaning the pod will not be able to start even if the image is cached on the node.
This solution does however not work when using Spegel, instead, Spegel may make the problem worse. Without Spegel an image that would want to use a private image, it does not have access to would have to be scheduled on a node that has already pulled the image.
With Spegel that image will be available to all nodes in the cluster. Currently, a good solution for Spegel does not exist. There are two reasons for this. The first is that credentials are not included when pulling an image from a registry mirror, a good choice as doing so would mean sharing credentials with third parties.
Additionally, Spegel would have no method of validating the credentials even if they were included in the requests. So for the time being if you have these types of requirements Spegel may not be the choice for you.
## How do I use Spegel in conjunction with another registry cache?
Spegel can be used with other registry caches in cases where the best effort caching offered by Spegel is not enough. In these situations, if the image is not cached within the cluster the image should be pulled from the secondary cache.
This is configured by adding the domain of the registry to the `additionalMirrorRegistries` list in the Helm values. Registries added to this list will be included in the mirror configuration created by Spegel.
```yaml
spegel:
additionalMirrorRegistries:
- https://zot.example.com
```

View File

@ -1,13 +0,0 @@
# Metrics
| Name| Type | Labels |
| ---------- | ----------- | ----------- |
| spegel_advertised_images | Gauge | `registry` |
| spegel_resolve_duration_seconds | Histogram | `router` |
| spegel_advertised_keys | Gauge | `registry` |
| spegel_advertised_image_tags | Gauge | `registry` |
| spegel_advertised_image_digests | Gauge | `registry` |
| spegel_mirror_requests_total | Counter | `registry` <br/> `cache=hit\|miss` <br/> `source=internal\|external` |
| http_request_duration_seconds | Histogram | `handler` <br/> `method` <br/> `code` |
| http_response_size_bytes | Histogram | `handler` <br/> `method` <br/> `code` |
| http_requests_inflight | Gauge | `handler` |

262
go.mod
View File

@ -1,120 +1,109 @@
module github.com/spegel-org/spegel
go 1.21
go 1.24.0
toolchain go1.24.3
require (
github.com/alexflint/go-arg v1.4.3
github.com/containerd/containerd v1.7.15
github.com/containerd/typeurl/v2 v2.1.1
github.com/go-logr/logr v1.4.1
github.com/go-logr/zapr v1.3.0
github.com/ipfs/go-cid v0.4.1
github.com/libp2p/go-libp2p v0.33.2
github.com/libp2p/go-libp2p-kad-dht v0.25.2
github.com/multiformats/go-multiaddr v0.12.3
github.com/multiformats/go-multicodec v0.9.0
cuelabs.dev/go/oci/ociregistry v0.0.0-20250530080122-d0efc28a5723
github.com/alexflint/go-arg v1.5.1
github.com/containerd/containerd/api v1.9.0
github.com/containerd/containerd/v2 v2.1.1
github.com/containerd/errdefs v1.0.0
github.com/containerd/typeurl/v2 v2.2.3
github.com/go-logr/logr v1.4.3
github.com/ipfs/go-cid v0.5.0
github.com/libp2p/go-libp2p v0.41.1
github.com/libp2p/go-libp2p-kad-dht v0.33.1
github.com/multiformats/go-multiaddr v0.16.0
github.com/multiformats/go-multicodec v0.9.1
github.com/multiformats/go-multihash v0.2.3
github.com/norwoodj/helm-docs v1.12.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0
github.com/pelletier/go-toml/v2 v2.2.0
github.com/prometheus/client_golang v1.19.0
github.com/spf13/afero v1.11.0
github.com/stretchr/testify v1.9.0
go.etcd.io/bbolt v1.3.9
go.uber.org/zap v1.27.0
golang.org/x/sync v0.7.0
golang.org/x/time v0.5.0
k8s.io/client-go v0.28.8
k8s.io/cri-api v0.28.8
k8s.io/klog/v2 v2.100.1
github.com/opencontainers/image-spec v1.1.1
github.com/pelletier/go-toml/v2 v2.2.4
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/common v0.64.0
github.com/stretchr/testify v1.10.0
go.etcd.io/bbolt v1.4.1
golang.org/x/sync v0.15.0
google.golang.org/grpc v1.73.0
k8s.io/apimachinery v0.33.1
k8s.io/cri-api v0.33.1
k8s.io/klog/v2 v2.130.1
)
require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.13.0 // indirect
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/containerd/continuity v0.4.2 // indirect
github.com/containerd/cgroups/v3 v3.0.5 // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/fifo v1.1.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/ttrpc v1.2.3 // indirect
github.com/containerd/platforms v1.0.0-rc.1 // indirect
github.com/containerd/plugin v1.0.0 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/elastic/gosigar v0.14.2 // indirect
github.com/emicklei/go-restful/v3 v3.10.2 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/elastic/gosigar v0.14.3 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/flynn/noise v1.1.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/boxo v0.10.0 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.5.1 // indirect
github.com/ipld/go-ipld-prime v0.20.0 // indirect
github.com/ipfs/boxo v0.30.0 // indirect
github.com/ipfs/go-datastore v0.8.2 // indirect
github.com/ipfs/go-log/v2 v2.6.0 // indirect
github.com/ipld/go-ipld-prime v0.21.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/koron/go-ssdp v0.0.5 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-flow-metrics v0.1.0 // indirect
github.com/libp2p/go-flow-metrics v0.2.0 // indirect
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
github.com/libp2p/go-libp2p-kbucket v0.6.3 // indirect
github.com/libp2p/go-libp2p-record v0.2.0 // indirect
github.com/libp2p/go-libp2p-routing-helpers v0.7.2 // indirect
github.com/libp2p/go-libp2p-kbucket v0.7.0 // indirect
github.com/libp2p/go-libp2p-record v0.3.1 // indirect
github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/libp2p/go-nat v0.2.0 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/libp2p/go-netroute v0.2.2 // indirect
github.com/libp2p/go-reuseport v0.4.0 // indirect
github.com/libp2p/go-yamux/v4 v4.0.1 // indirect
github.com/libp2p/go-yamux/v5 v5.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/miekg/dns v1.1.66 // indirect
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
@ -122,81 +111,90 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/signal v0.7.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect
github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multistream v0.5.0 // indirect
github.com/multiformats/go-multistream v0.6.0 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.15.0 // indirect
github.com/opencontainers/runtime-spec v1.2.0 // indirect
github.com/opencontainers/selinux v1.11.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/norwoodj/helm-docs v1.14.2 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/opencontainers/runtime-spec v1.2.1 // indirect
github.com/opencontainers/selinux v1.12.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/ice/v4 v4.0.8 // indirect
github.com/pion/interceptor v0.1.39 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.18 // indirect
github.com/pion/sctp v1.8.37 // indirect
github.com/pion/sdp/v3 v3.0.10 // indirect
github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/stun v0.6.1 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pion/webrtc/v4 v4.0.10 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/polydawn/refmt v0.89.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/quic-go v0.42.0 // indirect
github.com/quic-go/webtransport-go v0.6.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.50.1 // indirect
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
github.com/raulk/go-watchdog v1.3.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
go.uber.org/dig v1.17.1 // indirect
go.uber.org/fx v1.20.1 // indirect
go.uber.org/mock v0.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/dig v1.18.0 // indirect
go.uber.org/fx v1.23.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.18.0 // indirect
gonum.org/v1/gonum v0.13.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.33.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.28.8 // indirect
k8s.io/apimachinery v0.28.8 // indirect
k8s.io/helm v2.17.0+incompatible // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
helm.sh/helm/v3 v3.17.3 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)
tool github.com/norwoodj/helm-docs/cmd/helm-docs

2104
go.sum

File diff suppressed because it is too large Load Diff

145
internal/cleanup/cleanup.go Normal file
View File

@ -0,0 +1,145 @@
package cleanup
import (
"context"
"errors"
"net"
"net/http"
"net/url"
"time"
"github.com/go-logr/logr"
"golang.org/x/sync/errgroup"
"github.com/spegel-org/spegel/internal/channel"
"github.com/spegel-org/spegel/pkg/httpx"
"github.com/spegel-org/spegel/pkg/oci"
)
func Run(ctx context.Context, addr, configPath string) error {
log := logr.FromContextOrDiscard(ctx)
err := oci.CleanupMirrorConfiguration(ctx, configPath)
if err != nil {
return err
}
g, gCtx := errgroup.WithContext(ctx)
mux := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet && req.URL.Path != "/healthz" {
log.Error(errors.New("unknown request"), "unsupported probe request", "path", req.URL.Path, "method", req.Method)
rw.WriteHeader(http.StatusNotFound)
return
}
rw.WriteHeader(http.StatusOK)
})
srv := &http.Server{
Addr: addr,
Handler: mux,
}
g.Go(func() error {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
g.Go(func() error {
<-gCtx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return srv.Shutdown(shutdownCtx)
})
log.Info("waiting to be shutdown")
err = g.Wait()
if err != nil {
return err
}
return nil
}
func Wait(ctx context.Context, probeEndpoint string, period time.Duration, threshold int) error {
log := logr.FromContextOrDiscard(ctx)
resolver := &net.Resolver{}
httpClient := httpx.BaseClient()
addr, port, err := net.SplitHostPort(probeEndpoint)
if err != nil {
return err
}
immediateCh := make(chan time.Time, 1)
immediateCh <- time.Now()
close(immediateCh)
ticker := time.NewTicker(period)
defer ticker.Stop()
tickerCh := channel.Merge(immediateCh, ticker.C)
thresholdCount := 0
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-tickerCh:
start := time.Now()
log.Info("running probe lookup", "host", addr)
ips, err := resolver.LookupIPAddr(ctx, addr)
if err != nil {
log.Error(err, "cleanup probe lookup failed")
thresholdCount = 0
continue
}
log.Info("running probe request", "endpoints", len(ips))
err = probeIPs(ctx, httpClient, ips, port)
if err != nil {
log.Error(err, "cleanup probe request failed")
thresholdCount = 0
continue
}
thresholdCount += 1
log.Info("probe ran successfully", "threshold", thresholdCount, "duration", time.Since(start).String())
if thresholdCount == threshold {
log.Info("probe threshold reached")
return nil
}
}
}
}
func probeIPs(ctx context.Context, client *http.Client, ips []net.IPAddr, port string) error {
g, gCtx := errgroup.WithContext(ctx)
g.SetLimit(10)
for _, ip := range ips {
g.Go(func() error {
u := url.URL{
Scheme: "http",
Host: net.JoinHostPort(ip.String(), port),
Path: "/healthz",
}
reqCtx, cancel := context.WithTimeout(gCtx, 1*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, u.String(), nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer httpx.DrainAndClose(resp.Body)
err = httpx.CheckResponseStatus(resp, http.StatusOK)
if err != nil {
return err
}
return nil
})
}
err := g.Wait()
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,61 @@
package cleanup
import (
"context"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)
func TestCleanupFail(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
u, err := url.Parse(srv.URL)
require.NoError(t, err)
timeoutCtx, timeoutCancel := context.WithTimeout(t.Context(), 1*time.Second)
defer timeoutCancel()
err = Wait(timeoutCtx, u.Host, 100*time.Millisecond, 3)
require.EqualError(t, err, "context deadline exceeded")
}
func TestCleanupSucceed(t *testing.T) {
t.Parallel()
listener, err := net.Listen("tcp", ":0")
if err != nil {
panic(err)
}
addr := listener.Addr().String()
err = listener.Close()
require.NoError(t, err)
timeoutCtx, timeoutCancel := context.WithTimeout(t.Context(), 1*time.Second)
defer timeoutCancel()
g, gCtx := errgroup.WithContext(timeoutCtx)
g.Go(func() error {
err := Run(gCtx, addr, t.TempDir())
if err != nil {
return err
}
return nil
})
g.Go(func() error {
err := Wait(gCtx, addr, 100*time.Microsecond, 3)
if err != nil {
return err
}
return nil
})
err = g.Wait()
require.NoError(t, err)
}

View File

@ -1,30 +0,0 @@
package kubernetes
import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
func GetClientset(kubeconfigPath string) (kubernetes.Interface, error) {
if kubeconfigPath != "" {
cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
if err != nil {
return nil, err
}
clientset, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, err
}
return clientset, nil
}
config, err := rest.InClusterConfig()
if err != nil {
return nil, err
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
return clientset, nil
}

View File

@ -1,17 +0,0 @@
package mux
import "net/http"
type Handler func(rw ResponseWriter, req *http.Request)
type ServeMux struct {
h Handler
}
func NewServeMux(handler Handler) *ServeMux {
return &ServeMux{h: handler}
}
func (s *ServeMux) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
s.h(&response{ResponseWriter: rw}, req)
}

View File

@ -1,15 +0,0 @@
package mux
import (
"io"
"net/http"
"testing"
"github.com/stretchr/testify/require"
)
func TestResponseWriter(t *testing.T) {
var rw http.ResponseWriter = &response{}
_, ok := rw.(io.ReaderFrom)
require.True(t, ok)
}

View File

@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spegel Debug</title>
<link rel="icon" href="https://spegel.dev/favicon.svg" type="image/svg+xml">
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<style>
body {
margin: 0;
padding: 0;
font-family: ui-sans-serif, system-ui, sans-serif, "apple color emoji", "segoe ui emoji", "segoe ui symbol", "noto color emoji";
font-size: 16px;
}
.container {
max-width: 1366px;
width: 100%;
margin: 0 auto;
padding: 0 20px;
}
.table-container {
max-width: 100%;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
text-align: left;
padding: 8px;
border: 1px solid #ddd;
}
th {
background-color: #f4f4f4;
font-weight: bold;
}
.stat-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
width: 100%;
margin-bottom: 16px;
}
.stat-box {
padding: 16px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
text-align: center;
}
.stat-title {
font-size: 14px;
color: #555;
}
.stat-value {
font-size: 24px;
font-weight: bold;
margin-top: 8px;
}
.measure-container {
padding: 16px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
input[type="text"],
button {
font-size: 16px;
height: 2em;
padding: 0 8px;
border: 1px solid #ccc;
}
input[type="text"] {
width: 100%;
max-width: 450px;
}
button {
background-color: #1d5a9a;
color: white;
border: none;
cursor: pointer;
padding: 0 12px;
}
button:hover {
background-color: #164577;
}
</style>
</head>
<body>
<div class="container">
<h1>Spegel</h1>
<div hx-get="/debug/web/stats" hx-trigger="load, every 2s"></div>
<div class="measure-container">
<h2>Measure Image Pull</h2>
<form hx-get="/debug/web/measure" hx-target="#measure-result">
<input type="text" name="image" placeholder="ghcr.io/spegel-org/spegel:v0.30.0" />
<button>Pull</button>
</form>
<div id="measure-result"></div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,48 @@
{{ if .PeerResults }}
<h3>Resolved Peers</h3>
<div style="margin-bottom: 10px;">
<strong>Duration:</strong> {{ .PeerDuration | formatDuration }}
</div>
<div class="table-container">
<table>
<tr>
<th style="width: 50%;">Peer</th>
<th style="width: 50%;">Duration</th>
</tr>
{{ range .PeerResults }}
<tr>
<td>{{ .Peer.Addr }}</td>
<td>{{ .Duration | formatDuration }}</td>
</tr>
{{ end }}
</table>
</div>
<h3>Result</h3>
<div style="margin-bottom: 10px;">
<strong>Duration:</strong> {{ .PullDuration | formatDuration }}
<strong>Size:</strong> {{ .PullSize | formatBytes }}
</div>
<div class="table-container">
<table>
<tr>
<th>Identifier</th>
<th>Type</th>
<th>Size</th>
<th>Duration</th>
</tr>
{{ range .PullResults }}
<tr>
<td>{{ .Identifier }}</td>
<td>{{ .Type }}</td>
<td>{{ .Size | formatBytes }}</td>
<td>{{ .Duration | formatDuration }}</td>
</tr>
{{ end }}
</table>
</div>
{{ else }}
<p>No peers found for image</p>
{{ end }}

View File

@ -0,0 +1,12 @@
<div>
<div class="stat-container">
<div class="stat-box">
<div class="stat-title">Images</div>
<div class="stat-value">{{ .ImageCount }}</div>
</div>
<div class="stat-box">
<div class="stat-title">Layers</div>
<div class="stat-value">{{ .LayerCount }}</div>
</div>
</div>
</div>

220
internal/web/web.go Normal file
View File

@ -0,0 +1,220 @@
package web
import (
"embed"
"errors"
"fmt"
"html/template"
"net"
"net/http"
"net/netip"
"net/url"
"time"
"github.com/go-logr/logr"
"github.com/prometheus/common/expfmt"
"github.com/spegel-org/spegel/pkg/httpx"
"github.com/spegel-org/spegel/pkg/oci"
"github.com/spegel-org/spegel/pkg/routing"
)
//go:embed templates/*
var templatesFS embed.FS
type Web struct {
router routing.Router
ociClient *oci.Client
httpClient *http.Client
tmpls *template.Template
}
func NewWeb(router routing.Router, ociClient *oci.Client) (*Web, error) {
funcs := template.FuncMap{
"formatBytes": formatBytes,
"formatDuration": formatDuration,
}
tmpls, err := template.New("").Funcs(funcs).ParseFS(templatesFS, "templates/*")
if err != nil {
return nil, err
}
return &Web{
router: router,
ociClient: ociClient,
httpClient: httpx.BaseClient(),
tmpls: tmpls,
}, nil
}
func (w *Web) Handler(log logr.Logger) http.Handler {
m := httpx.NewServeMux(log)
m.Handle("GET /debug/web/", w.indexHandler)
m.Handle("GET /debug/web/stats", w.statsHandler)
m.Handle("GET /debug/web/measure", w.measureHandler)
return m
}
func (w *Web) indexHandler(rw httpx.ResponseWriter, req *http.Request) {
err := w.tmpls.ExecuteTemplate(rw, "index.html", nil)
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
return
}
}
func (w *Web) statsHandler(rw httpx.ResponseWriter, req *http.Request) {
//nolint: errcheck // Ignore error.
srvAddr := req.Context().Value(http.LocalAddrContextKey).(net.Addr)
req, err := http.NewRequestWithContext(req.Context(), http.MethodGet, fmt.Sprintf("http://%s/metrics", srvAddr.String()), nil)
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
return
}
resp, err := w.httpClient.Do(req)
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
return
}
defer httpx.DrainAndClose(resp.Body)
parser := expfmt.TextParser{}
metricFamilies, err := parser.TextToMetricFamilies(resp.Body)
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
return
}
data := struct {
ImageCount int64
LayerCount int64
}{}
for _, metric := range metricFamilies["spegel_advertised_images"].Metric {
data.ImageCount += int64(*metric.Gauge.Value)
}
for _, metric := range metricFamilies["spegel_advertised_keys"].Metric {
data.LayerCount += int64(*metric.Gauge.Value)
}
err = w.tmpls.ExecuteTemplate(rw, "stats.html", data)
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
return
}
}
type measureResult struct {
PeerResults []peerResult
PullResults []pullResult
PeerDuration time.Duration
PullDuration time.Duration
PullSize int64
}
type peerResult struct {
Peer netip.AddrPort
Duration time.Duration
}
type pullResult struct {
Identifier string
Type string
Size int64
Duration time.Duration
}
func (w *Web) measureHandler(rw httpx.ResponseWriter, req *http.Request) {
mirror := &url.URL{
Scheme: "http",
Host: "localhost:5000",
}
// Parse image name.
imgName := req.URL.Query().Get("image")
if imgName == "" {
rw.WriteError(http.StatusBadRequest, errors.New("image name cannot be empty"))
return
}
img, err := oci.ParseImage(imgName)
if err != nil {
rw.WriteError(http.StatusBadRequest, err)
return
}
res := measureResult{}
// Resolve peers for the given image.
resolveStart := time.Now()
peerCh, err := w.router.Resolve(req.Context(), imgName, 0)
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
return
}
for peer := range peerCh {
d := time.Since(resolveStart)
res.PeerDuration += d
res.PeerResults = append(res.PeerResults, peerResult{
Peer: peer,
Duration: d,
})
}
if len(res.PeerResults) > 0 {
// Pull the image and measure performance.
pullMetrics, err := w.ociClient.Pull(req.Context(), img, oci.WithFetchMirror(mirror))
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
return
}
for _, metric := range pullMetrics {
res.PullDuration += metric.Duration
res.PullSize += metric.ContentLength
res.PullResults = append(res.PullResults, pullResult{
Identifier: metric.Digest.String(),
Type: metric.ContentType,
Size: metric.ContentLength,
Duration: metric.Duration,
})
}
}
err = w.tmpls.ExecuteTemplate(rw, "measure.html", res)
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
return
}
}
func formatBytes(size int64) string {
const unit = 1024
if size < unit {
return fmt.Sprintf("%d B", size)
}
div, exp := int64(unit), 0
for n := size / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
}
func formatDuration(d time.Duration) string {
if d < time.Millisecond {
return "<1ms"
}
totalMs := int64(d / time.Millisecond)
minutes := totalMs / 60000
seconds := (totalMs % 60000) / 1000
milliseconds := totalMs % 1000
out := ""
if minutes > 0 {
out += fmt.Sprintf("%dm", minutes)
}
if seconds > 0 {
out += fmt.Sprintf("%ds", seconds)
}
if milliseconds > 0 {
out += fmt.Sprintf("%dms", milliseconds)
}
return out
}

80
internal/web/web_test.go Normal file
View File

@ -0,0 +1,80 @@
package web
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestWeb(t *testing.T) {
t.Parallel()
w, err := NewWeb(nil, nil)
require.NoError(t, err)
require.NotNil(t, w.tmpls)
}
func TestFormatBytes(t *testing.T) {
t.Parallel()
tests := []struct {
expected string
size int64
}{
{
size: 1,
expected: "1 B",
},
{
size: 19456,
expected: "19.0 KB",
},
{
size: 1073741824,
expected: "1.0 GB",
},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
t.Parallel()
result := formatBytes(tt.size)
require.Equal(t, tt.expected, result)
})
}
}
func TestDuration(t *testing.T) {
t.Parallel()
tests := []struct {
expected string
duration time.Duration
}{
{
duration: 36 * time.Millisecond,
expected: "36ms",
},
{
duration: 5 * time.Microsecond,
expected: "<1ms",
},
{
duration: 5*time.Minute + 128*time.Second,
expected: "7m8s",
},
{
duration: 2 * time.Hour,
expected: "120m",
},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
t.Parallel()
result := formatDuration(tt.duration)
require.Equal(t, tt.expected, result)
})
}
}

276
main.go
View File

@ -4,87 +4,100 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"net/http/pprof"
"net/url"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/alexflint/go-arg"
"github.com/go-logr/logr"
"github.com/go-logr/zapr"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/afero"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"k8s.io/klog/v2"
"github.com/spegel-org/spegel/internal/kubernetes"
"github.com/spegel-org/spegel/internal/cleanup"
"github.com/spegel-org/spegel/internal/web"
"github.com/spegel-org/spegel/pkg/metrics"
"github.com/spegel-org/spegel/pkg/oci"
"github.com/spegel-org/spegel/pkg/registry"
"github.com/spegel-org/spegel/pkg/routing"
"github.com/spegel-org/spegel/pkg/state"
"github.com/spegel-org/spegel/pkg/throttle"
)
type ConfigurationCmd struct {
ContainerdRegistryConfigPath string `arg:"--containerd-registry-config-path,env:CONTAINERD_REGISTRY_CONFIG_PATH" default:"/etc/containerd/certs.d" help:"Directory where mirror configuration is written."`
Registries []url.URL `arg:"--registries,required,env:REGISTRIES" help:"registries that are configured to be mirrored."`
MirrorRegistries []url.URL `arg:"--mirror-registries,env:MIRROR_REGISTRIES,required" help:"registries that are configured to act as mirrors."`
MirroredRegistries []url.URL `arg:"--mirrored-registries,env:MIRRORED_REGISTRIES" help:"Registries that are configured to be mirrored, if slice is empty all registires are mirrored."`
MirrorTargets []url.URL `arg:"--mirror-targets,env:MIRROR_TARGETS,required" help:"registries that are configured to act as mirrors."`
ResolveTags bool `arg:"--resolve-tags,env:RESOLVE_TAGS" default:"true" help:"When true Spegel will resolve tags to digests."`
AppendMirrors bool `arg:"--append-mirrors,env:APPEND_MIRRORS" default:"false" help:"When true existing mirror configuration will be appended to instead of replaced."`
PrependExisting bool `arg:"--prepend-existing,env:PREPEND_EXISTING" default:"false" help:"When true existing mirror configuration will be kept and Spegel will prepend it's configuration."`
}
type BootstrapConfig struct {
BootstrapKind string `arg:"--bootstrap-kind,env:BOOTSTRAP_KIND" help:"Kind of bootsrapper to use."`
HTTPBootstrapAddr string `arg:"--http-bootstrap-addr,env:HTTP_BOOTSTRAP_KIND" help:"Address to serve for HTTP bootstrap."`
HTTPBootstrapPeer string `arg:"--http-bootstrap-peer,env:HTTP_BOOTSTRAP_PEER" help:"Peer to HTTP bootstrap with."`
KubeconfigPath string `arg:"--kubeconfig-path,env:KUBECONFIG_PATH" help:"Path to the kubeconfig file."`
LeaderElectionName string `arg:"--leader-election-name,env:LEADER_ELECTION_NAME" default:"spegel-leader-election" help:"Name of leader election."`
LeaderElectionNamespace string `arg:"--leader-election-namespace,env:LEADER_ELECTION_NAMESPACE" default:"spegel" help:"Kubernetes namespace to write leader election data."`
BootstrapKind string `arg:"--bootstrap-kind,env:BOOTSTRAP_KIND" help:"Kind of bootsrapper to use."`
DNSBootstrapDomain string `arg:"--dns-bootstrap-domain,env:DNS_BOOTSTRAP_DOMAIN" help:"Domain to use when bootstrapping using DNS."`
HTTPBootstrapAddr string `arg:"--http-bootstrap-addr,env:HTTP_BOOTSTRAP_ADDR" help:"Address to serve for HTTP bootstrap."`
HTTPBootstrapPeer string `arg:"--http-bootstrap-peer,env:HTTP_BOOTSTRAP_PEER" help:"Peer to HTTP bootstrap with."`
StaticBootstrapPeers []string `arg:"--static-bootstrap-peers,env:STATIC_BOOTSTRAP_PEERS" help:"Static list of peers to bootstrap with."`
}
type RegistryCmd struct {
BootstrapConfig
BlobSpeed *throttle.Byterate `arg:"--blob-speed,env:BLOB_SPEED" help:"Maximum write speed per request when serving blob layers. Should be an integer followed by unit Bps, KBps, MBps, GBps, or TBps."`
ContainerdRegistryConfigPath string `arg:"--containerd-registry-config-path,env:CONTAINERD_REGISTRY_CONFIG_PATH" default:"/etc/containerd/certs.d" help:"Directory where mirror configuration is written."`
MetricsAddr string `arg:"--metrics-addr,required,env:METRICS_ADDR" help:"address to serve metrics."`
LocalAddr string `arg:"--local-addr,required,env:LOCAL_ADDR" help:"Address that the local Spegel instance will be reached at."`
ContainerdSock string `arg:"--containerd-sock,env:CONTAINERD_SOCK" default:"/run/containerd/containerd.sock" help:"Endpoint of containerd service."`
ContainerdNamespace string `arg:"--containerd-namespace,env:CONTAINERD_NAMESPACE" default:"k8s.io" help:"Containerd namespace to fetch images from."`
ContainerdContentPath string `arg:"--containerd-content-path,env:CONTAINERD_CONTENT_PATH" default:"/var/lib/containerd/io.containerd.content.v1.content" help:"Path to Containerd content store"`
RouterAddr string `arg:"--router-addr,env:ROUTER_ADDR,required" help:"address to serve router."`
RegistryAddr string `arg:"--registry-addr,env:REGISTRY_ADDR,required" help:"address to server image registry."`
Registries []url.URL `arg:"--registries,env:REGISTRIES,required" help:"registries that are configured to be mirrored."`
MirrorResolveTimeout time.Duration `arg:"--mirror-resolve-timeout,env:MIRROR_RESOLVE_TIMEOUT" default:"5s" help:"Max duration spent finding a mirror."`
MirrorResolveRetries int `arg:"--mirror-resolve-retries,env:MIRROR_RESOLVE_RETRIES" default:"3" help:"Max amount of mirrors to attempt."`
ResolveLatestTag bool `arg:"--resolve-latest-tag,env:RESOLVE_LATEST_TAG" default:"true" help:"When true latest tags will be resolved to digests."`
ContainerdRegistryConfigPath string `arg:"--containerd-registry-config-path,env:CONTAINERD_REGISTRY_CONFIG_PATH" default:"/etc/containerd/certs.d" help:"Directory where mirror configuration is written."`
MetricsAddr string `arg:"--metrics-addr,env:METRICS_ADDR" default:":9090" help:"address to serve metrics."`
ContainerdSock string `arg:"--containerd-sock,env:CONTAINERD_SOCK" default:"/run/containerd/containerd.sock" help:"Endpoint of containerd service."`
ContainerdNamespace string `arg:"--containerd-namespace,env:CONTAINERD_NAMESPACE" default:"k8s.io" help:"Containerd namespace to fetch images from."`
ContainerdContentPath string `arg:"--containerd-content-path,env:CONTAINERD_CONTENT_PATH" default:"/var/lib/containerd/io.containerd.content.v1.content" help:"Path to Containerd content store"`
DataDir string `arg:"--data-dir,env:DATA_DIR" default:"/var/lib/spegel" help:"Directory where Spegel persists data."`
RouterAddr string `arg:"--router-addr,env:ROUTER_ADDR" default:":5001" help:"address to serve router."`
RegistryAddr string `arg:"--registry-addr,env:REGISTRY_ADDR" default:":5000" help:"address to server image registry."`
MirroredRegistries []url.URL `arg:"--mirrored-registries,env:MIRRORED_REGISTRIES" help:"Registries that are configured to be mirrored, if slice is empty all registires are mirrored."`
MirrorResolveTimeout time.Duration `arg:"--mirror-resolve-timeout,env:MIRROR_RESOLVE_TIMEOUT" default:"20ms" help:"Max duration spent finding a mirror."`
MirrorResolveRetries int `arg:"--mirror-resolve-retries,env:MIRROR_RESOLVE_RETRIES" default:"3" help:"Max amount of mirrors to attempt."`
ResolveLatestTag bool `arg:"--resolve-latest-tag,env:RESOLVE_LATEST_TAG" default:"true" help:"When true latest tags will be resolved to digests."`
DebugWebEnabled bool `arg:"--debug-web-enabled,env:DEBUG_WEB_ENABLED" default:"false" help:"When true enables debug web page."`
}
type CleanupCmd struct {
Addr string `arg:"--addr,required,env:ADDR" help:"address to run readiness probe on."`
ContainerdRegistryConfigPath string `arg:"--containerd-registry-config-path,env:CONTAINERD_REGISTRY_CONFIG_PATH" default:"/etc/containerd/certs.d" help:"Directory where mirror configuration is written."`
}
type CleanupWaitCmd struct {
ProbeEndpoint string `arg:"--probe-endpoint,required,env:PROBE_ENDPOINT" help:"endpoint to probe cleanup jobs from."`
Threshold int `arg:"--threshold,env:THRESHOLD" default:"3" help:"amount of consecutive successful probes to consider cleanup done."`
Period time.Duration `arg:"--period,env:PERIOD" default:"2s" help:"address to run readiness probe on."`
}
type Arguments struct {
Configuration *ConfigurationCmd `arg:"subcommand:configuration"`
Registry *RegistryCmd `arg:"subcommand:registry"`
Cleanup *CleanupCmd `arg:"subcommand:cleanup"`
CleanupWait *CleanupWaitCmd `arg:"subcommand:cleanup-wait"`
LogLevel slog.Level `arg:"--log-level,env:LOG_LEVEL" default:"INFO" help:"Minimum log level to output. Value should be DEBUG, INFO, WARN, or ERROR."`
}
func main() {
args := &Arguments{}
arg.MustParse(args)
zapLog, err := zap.NewProduction()
if err != nil {
panic(fmt.Sprintf("who watches the watchmen (%v)?", err))
opts := slog.HandlerOptions{
AddSource: true,
Level: args.LogLevel,
}
log := zapr.NewLogger(zapLog)
handler := slog.NewJSONHandler(os.Stderr, &opts)
log := logr.FromSlogHandler(handler)
klog.SetLogger(log)
ctx := logr.NewContext(context.Background(), log)
err = run(ctx, args)
err := run(ctx, args)
if err != nil {
log.Error(err, "")
log.Error(err, "run exit with error")
os.Exit(1)
}
log.Info("gracefully shutdown")
@ -98,14 +111,21 @@ func run(ctx context.Context, args *Arguments) error {
return configurationCommand(ctx, args.Configuration)
case args.Registry != nil:
return registryCommand(ctx, args.Registry)
case args.Cleanup != nil:
return cleanupCommand(ctx, args.Cleanup)
case args.CleanupWait != nil:
return cleanupWaitCommand(ctx, args.CleanupWait)
default:
return fmt.Errorf("unknown subcommand")
return errors.New("unknown subcommand")
}
}
func configurationCommand(ctx context.Context, args *ConfigurationCmd) error {
fs := afero.NewOsFs()
err := oci.AddMirrorConfiguration(ctx, fs, args.ContainerdRegistryConfigPath, args.Registries, args.MirrorRegistries, args.ResolveTags, args.AppendMirrors)
username, password, err := loadBasicAuth()
if err != nil {
return err
}
err = oci.AddMirrorConfiguration(ctx, args.ContainerdRegistryConfigPath, args.MirroredRegistries, args.MirrorTargets, args.ResolveTags, args.PrependExisting, username, password)
if err != nil {
return err
}
@ -116,17 +136,82 @@ func registryCommand(ctx context.Context, args *RegistryCmd) (err error) {
log := logr.FromContextOrDiscard(ctx)
g, ctx := errgroup.WithContext(ctx)
// OCI Client
ociClient, err := oci.NewContainerd(args.ContainerdSock, args.ContainerdNamespace, args.ContainerdRegistryConfigPath, args.Registries, oci.WithContentPath(args.ContainerdContentPath))
if err != nil {
return err
}
err = ociClient.Verify(ctx)
username, password, err := loadBasicAuth()
if err != nil {
return err
}
// Metrics
ociClient := oci.NewClient()
// OCI Store
ociStore, err := oci.NewContainerd(args.ContainerdSock, args.ContainerdNamespace, args.ContainerdRegistryConfigPath, args.MirroredRegistries, oci.WithContentPath(args.ContainerdContentPath))
if err != nil {
return err
}
err = ociStore.Verify(ctx)
if err != nil {
return err
}
// Router
_, registryPort, err := net.SplitHostPort(args.RegistryAddr)
if err != nil {
return err
}
bootstrapper, err := getBootstrapper(args.BootstrapConfig)
if err != nil {
return err
}
routerOpts := []routing.P2PRouterOption{
routing.WithDataDir(args.DataDir),
}
router, err := routing.NewP2PRouter(ctx, args.RouterAddr, bootstrapper, registryPort, routerOpts...)
if err != nil {
return err
}
g.Go(func() error {
return router.Run(ctx)
})
// State tracking
g.Go(func() error {
err := state.Track(ctx, ociStore, router, args.ResolveLatestTag)
if err != nil {
return err
}
return nil
})
// Registry
registryOpts := []registry.RegistryOption{
registry.WithResolveLatestTag(args.ResolveLatestTag),
registry.WithResolveRetries(args.MirrorResolveRetries),
registry.WithResolveTimeout(args.MirrorResolveTimeout),
registry.WithLogger(log),
registry.WithBasicAuth(username, password),
}
reg, err := registry.NewRegistry(ociStore, router, registryOpts...)
if err != nil {
return err
}
regSrv, err := reg.Server(args.RegistryAddr)
if err != nil {
return err
}
g.Go(func() error {
if err := regSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
g.Go(func() error {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return regSrv.Shutdown(shutdownCtx)
})
// Metrics, pprof, and debug web
metrics.Register()
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.HandlerFor(metrics.DefaultGatherer, promhttp.HandlerOpts{}))
@ -135,10 +220,18 @@ func registryCommand(ctx context.Context, args *RegistryCmd) (err error) {
mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
if args.DebugWebEnabled {
web, err := web.NewWeb(router, ociClient)
if err != nil {
return err
}
mux.Handle("/debug/web/", web.Handler(log))
}
metricsSrv := &http.Server{
Addr: args.MetricsAddr,
Handler: mux,
@ -156,62 +249,6 @@ func registryCommand(ctx context.Context, args *RegistryCmd) (err error) {
return metricsSrv.Shutdown(shutdownCtx)
})
// Router
_, registryPort, err := net.SplitHostPort(args.RegistryAddr)
if err != nil {
return err
}
bootstrapper, err := getBootstrapper(args.BootstrapConfig)
if err != nil {
return err
}
router, err := routing.NewP2PRouter(ctx, args.RouterAddr, bootstrapper, registryPort)
if err != nil {
return err
}
g.Go(func() error {
return router.Run(ctx)
})
g.Go(func() error {
<-ctx.Done()
return router.Close()
})
// State tracking
g.Go(func() error {
err := state.Track(ctx, ociClient, router, args.ResolveLatestTag)
if err != nil {
return err
}
return nil
})
// Registry
registryOpts := []registry.Option{
registry.WithResolveLatestTag(args.ResolveLatestTag),
registry.WithResolveRetries(args.MirrorResolveRetries),
registry.WithResolveTimeout(args.MirrorResolveTimeout),
registry.WithLocalAddress(args.LocalAddr),
registry.WithLogger(log),
}
if args.BlobSpeed != nil {
registryOpts = append(registryOpts, registry.WithBlobSpeed(*args.BlobSpeed))
}
reg := registry.NewRegistry(ociClient, router, registryOpts...)
regSrv := reg.Server(args.RegistryAddr)
g.Go(func() error {
if err := regSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
g.Go(func() error {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return regSrv.Shutdown(shutdownCtx)
})
log.Info("running Spegel", "registry", args.RegistryAddr, "router", args.RouterAddr)
err = g.Wait()
if err != nil {
@ -220,17 +257,44 @@ func registryCommand(ctx context.Context, args *RegistryCmd) (err error) {
return nil
}
func getBootstrapper(cfg BootstrapConfig) (routing.Bootstrapper, error) {
func cleanupCommand(ctx context.Context, args *CleanupCmd) error {
err := cleanup.Run(ctx, args.Addr, args.ContainerdRegistryConfigPath)
if err != nil {
return err
}
return nil
}
func cleanupWaitCommand(ctx context.Context, args *CleanupWaitCmd) error {
err := cleanup.Wait(ctx, args.ProbeEndpoint, args.Period, args.Threshold)
if err != nil {
return err
}
return nil
}
func getBootstrapper(cfg BootstrapConfig) (routing.Bootstrapper, error) { //nolint: ireturn // Return type can be different structs.
switch cfg.BootstrapKind {
case "dns":
return routing.NewDNSBootstrapper(cfg.DNSBootstrapDomain, 10), nil
case "http":
return routing.NewHTTPBootstrapper(cfg.HTTPBootstrapAddr, cfg.HTTPBootstrapPeer), nil
case "kubernetes":
cs, err := kubernetes.GetClientset(cfg.KubeconfigPath)
if err != nil {
return nil, err
}
return routing.NewKubernetesBootstrapper(cs, cfg.LeaderElectionNamespace, cfg.LeaderElectionName), nil
case "static":
return routing.NewStaticBootstrapperFromStrings(cfg.StaticBootstrapPeers)
default:
return nil, fmt.Errorf("unknown bootstrap kind %s", cfg.BootstrapKind)
}
}
func loadBasicAuth() (string, string, error) {
dirPath := "/etc/secrets/basic-auth"
username, err := os.ReadFile(filepath.Join(dirPath, "username"))
if err != nil && !errors.Is(err, os.ErrNotExist) {
return "", "", err
}
password, err := os.ReadFile(filepath.Join(dirPath, "password"))
if err != nil && !errors.Is(err, os.ErrNotExist) {
return "", "", err
}
return string(username), string(password), nil
}

30
pkg/httpx/header.go Normal file
View File

@ -0,0 +1,30 @@
package httpx
import "net/http"
const (
HeaderContentType = "Content-Type"
HeaderContentLength = "Content-Length"
HeaderContentRange = "Content-Range"
HeaderRange = "Range"
HeaderAcceptRanges = "Accept-Ranges"
HeaderUserAgent = "User-Agent"
HeaderAccept = "Accept"
HeaderAuthorization = "Authorization"
HeaderWWWAuthenticate = "WWW-Authenticate"
HeaderXForwardedFor = "X-Forwarded-For"
)
const (
ContentTypeBinary = "application/octet-stream"
ContentTypeJSON = "application/json"
)
// CopyHeader copies header from source to destination.
func CopyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}

20
pkg/httpx/header_test.go Normal file
View File

@ -0,0 +1,20 @@
package httpx
import (
"net/http"
"testing"
"github.com/stretchr/testify/require"
)
func TestCopyHeader(t *testing.T) {
t.Parallel()
src := http.Header{
"foo": []string{"2", "1"},
}
dst := http.Header{}
CopyHeader(dst, src)
require.Equal(t, []string{"2", "1"}, dst.Values("foo"))
}

54
pkg/httpx/httpx.go Normal file
View File

@ -0,0 +1,54 @@
package httpx
import (
"errors"
"io"
"net"
"net/http"
"time"
)
// BaseClient returns a http client with reasonable defaults set.
func BaseClient() *http.Client {
return &http.Client{
Transport: BaseTransport(),
Timeout: 10 * time.Second,
}
}
// BaseTransport returns a http transport with reasonable defaults set.
func BaseTransport() *http.Transport {
return &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}
const (
// MaxReadBytes is the maximum amount of bytes read when draining a response or reading error message.
MaxReadBytes = 512 * 1024
)
// DrainAndCloses empties the body buffer before closing the body.
func DrainAndClose(rc io.ReadCloser) error {
errs := []error{}
n, err := io.Copy(io.Discard, io.LimitReader(rc, MaxReadBytes+1))
if err != nil {
errs = append(errs, err)
}
if n > MaxReadBytes {
errs = append(errs, errors.New("reader has more data than max read bytes"))
}
err = rc.Close()
if err != nil {
errs = append(errs, err)
}
return errors.Join(errs...)
}

45
pkg/httpx/httpx_test.go Normal file
View File

@ -0,0 +1,45 @@
package httpx
import (
"bytes"
"io"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestBaseClient(t *testing.T) {
t.Parallel()
c := BaseClient()
require.Equal(t, 10*time.Second, c.Timeout)
_, ok := c.Transport.(*http.Transport)
require.True(t, ok)
}
func TestBaseTransport(t *testing.T) {
t.Parallel()
BaseTransport()
}
func TestDrainAndClose(t *testing.T) {
t.Parallel()
buf := bytes.NewBuffer(nil)
err := DrainAndClose(io.NopCloser(buf))
require.NoError(t, err)
require.Empty(t, buf.Bytes())
buf = bytes.NewBuffer(make([]byte, MaxReadBytes))
err = DrainAndClose(io.NopCloser(buf))
require.NoError(t, err)
require.Empty(t, buf.Bytes())
buf = bytes.NewBuffer(make([]byte, MaxReadBytes+10))
err = DrainAndClose(io.NopCloser(buf))
require.EqualError(t, err, "reader has more data than max read bytes")
require.Len(t, buf.Bytes(), 9)
}

32
pkg/httpx/metrics.go Normal file
View File

@ -0,0 +1,32 @@
package httpx
import "github.com/prometheus/client_golang/prometheus"
var (
HttpRequestDurHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Subsystem: "http",
Name: "request_duration_seconds",
Help: "The latency of the HTTP requests.",
}, []string{"handler", "method", "code"})
HttpResponseSizeHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Subsystem: "http",
Name: "response_size_bytes",
Help: "The size of the HTTP responses.",
// 1kB up to 2GB
Buckets: prometheus.ExponentialBuckets(1024, 5, 10),
}, []string{"handler", "method", "code"})
HttpRequestsInflight = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Subsystem: "http",
Name: "requests_inflight",
Help: "The number of inflight requests being handled at the same time.",
}, []string{"handler"})
)
func RegisterMetrics(registerer prometheus.Registerer) {
if registerer == nil {
registerer = prometheus.DefaultRegisterer
}
registerer.MustRegister(HttpRequestDurHistogram)
registerer.MustRegister(HttpResponseSizeHistogram)
registerer.MustRegister(HttpRequestsInflight)
}

104
pkg/httpx/mux.go Normal file
View File

@ -0,0 +1,104 @@
package httpx
import (
"errors"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-logr/logr"
)
type HandlerFunc func(rw ResponseWriter, req *http.Request)
type ServeMux struct {
mux *http.ServeMux
log logr.Logger
}
func NewServeMux(log logr.Logger) *ServeMux {
return &ServeMux{
mux: http.NewServeMux(),
log: log,
}
}
func (s *ServeMux) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
h, pattern := s.mux.Handler(req)
if pattern == "" {
kvs := []any{
"path", req.URL.Path,
"status", http.StatusNotFound,
"method", req.Method,
"ip", GetClientIP(req),
}
s.log.Error(errors.New("page not found"), "", kvs...)
rw.WriteHeader(http.StatusNotFound)
return
}
h.ServeHTTP(rw, req)
}
func (s *ServeMux) Handle(pattern string, handler HandlerFunc) {
metricsPath := metricsFriendlyPath(pattern)
s.mux.HandleFunc(pattern, func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
rw := &response{ResponseWriter: w}
defer func() {
latency := time.Since(start)
statusCode := strconv.FormatInt(int64(rw.Status()), 10)
HttpRequestsInflight.WithLabelValues(metricsPath).Add(-1)
HttpRequestDurHistogram.WithLabelValues(metricsPath, req.Method, statusCode).Observe(latency.Seconds())
HttpResponseSizeHistogram.WithLabelValues(metricsPath, req.Method, statusCode).Observe(float64(rw.Size()))
// Ignore logging requests to healthz to reduce log noise
if req.URL.Path == "/healthz" {
return
}
kvs := []any{
"path", req.URL.Path,
"status", rw.Status(),
"method", req.Method,
"latency", latency.String(),
"ip", GetClientIP(req),
"handler", rw.handler,
}
if rw.Status() >= 200 && rw.Status() < 400 {
s.log.Info("", kvs...)
return
}
s.log.Error(rw.Error(), "", kvs...)
}()
HttpRequestsInflight.WithLabelValues(metricsPath).Add(1)
handler(rw, req)
})
}
func GetClientIP(req *http.Request) string {
forwardedFor := req.Header.Get(HeaderXForwardedFor)
if forwardedFor != "" {
comps := strings.Split(forwardedFor, ",")
if len(comps) > 1 {
return comps[0]
}
return forwardedFor
}
h, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return ""
}
return h
}
func metricsFriendlyPath(pattern string) string {
_, path, _ := strings.Cut(pattern, "/")
path = "/" + path
if strings.HasSuffix(path, "/") {
return path + "*"
}
return path
}

160
pkg/httpx/mux_test.go Normal file
View File

@ -0,0 +1,160 @@
package httpx
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-logr/logr"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/require"
)
func TestServeMux(t *testing.T) {
t.Parallel()
registerer := prometheus.NewRegistry()
RegisterMetrics(registerer)
m := NewServeMux(logr.Discard())
handlersCalled := []string{}
m.Handle("/exact", func(rw ResponseWriter, req *http.Request) {
handlersCalled = append(handlersCalled, "exact")
})
m.Handle("/prefix/", func(rw ResponseWriter, req *http.Request) {
handlersCalled = append(handlersCalled, "prefix")
})
paths := []string{"/prefix/", "/exact", "/exact/foo", "/prefix/bar"}
for _, path := range paths {
rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "http://localhost"+path, nil)
m.ServeHTTP(rw, req)
}
expectedHandlersCalled := []string{"prefix", "exact", "prefix"}
require.Equal(t, expectedHandlersCalled, handlersCalled)
expectedMetrics := `
# HELP http_requests_inflight The number of inflight requests being handled at the same time.
# TYPE http_requests_inflight gauge
http_requests_inflight{handler="/exact"} 0
http_requests_inflight{handler="/prefix/*"} 0
`
err := testutil.CollectAndCompare(HttpRequestsInflight, strings.NewReader(expectedMetrics))
require.NoError(t, err)
expectedMetrics = `
# HELP http_response_size_bytes The size of the HTTP responses.
# TYPE http_response_size_bytes histogram
http_response_size_bytes_bucket{code="200",handler="/exact",method="GET",le="1024"} 1
http_response_size_bytes_bucket{code="200",handler="/exact",method="GET",le="5120"} 1
http_response_size_bytes_bucket{code="200",handler="/exact",method="GET",le="25600"} 1
http_response_size_bytes_bucket{code="200",handler="/exact",method="GET",le="128000"} 1
http_response_size_bytes_bucket{code="200",handler="/exact",method="GET",le="640000"} 1
http_response_size_bytes_bucket{code="200",handler="/exact",method="GET",le="3.2e+06"} 1
http_response_size_bytes_bucket{code="200",handler="/exact",method="GET",le="1.6e+07"} 1
http_response_size_bytes_bucket{code="200",handler="/exact",method="GET",le="8e+07"} 1
http_response_size_bytes_bucket{code="200",handler="/exact",method="GET",le="4e+08"} 1
http_response_size_bytes_bucket{code="200",handler="/exact",method="GET",le="2e+09"} 1
http_response_size_bytes_bucket{code="200",handler="/exact",method="GET",le="+Inf"} 1
http_response_size_bytes_sum{code="200",handler="/exact",method="GET"} 0
http_response_size_bytes_count{code="200",handler="/exact",method="GET"} 1
http_response_size_bytes_bucket{code="200",handler="/prefix/*",method="GET",le="1024"} 2
http_response_size_bytes_bucket{code="200",handler="/prefix/*",method="GET",le="5120"} 2
http_response_size_bytes_bucket{code="200",handler="/prefix/*",method="GET",le="25600"} 2
http_response_size_bytes_bucket{code="200",handler="/prefix/*",method="GET",le="128000"} 2
http_response_size_bytes_bucket{code="200",handler="/prefix/*",method="GET",le="640000"} 2
http_response_size_bytes_bucket{code="200",handler="/prefix/*",method="GET",le="3.2e+06"} 2
http_response_size_bytes_bucket{code="200",handler="/prefix/*",method="GET",le="1.6e+07"} 2
http_response_size_bytes_bucket{code="200",handler="/prefix/*",method="GET",le="8e+07"} 2
http_response_size_bytes_bucket{code="200",handler="/prefix/*",method="GET",le="4e+08"} 2
http_response_size_bytes_bucket{code="200",handler="/prefix/*",method="GET",le="2e+09"} 2
http_response_size_bytes_bucket{code="200",handler="/prefix/*",method="GET",le="+Inf"} 2
http_response_size_bytes_sum{code="200",handler="/prefix/*",method="GET"} 0
http_response_size_bytes_count{code="200",handler="/prefix/*",method="GET"} 2
`
err = testutil.CollectAndCompare(HttpResponseSizeHistogram, strings.NewReader(expectedMetrics))
require.NoError(t, err)
}
func TestGetClientIP(t *testing.T) {
t.Parallel()
tests := []struct {
name string
request *http.Request
expected string
}{
{
name: "x forwarded for single",
request: &http.Request{
Header: http.Header{
HeaderXForwardedFor: []string{"localhost"},
},
},
expected: "localhost",
},
{
name: "x forwarded for multiple",
request: &http.Request{
Header: http.Header{
HeaderXForwardedFor: []string{"localhost,127.0.0.1"},
},
},
expected: "localhost",
},
{
name: "remote address",
request: &http.Request{
RemoteAddr: "127.0.0.1:9090",
},
expected: "127.0.0.1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ip := GetClientIP(tt.request)
require.Equal(t, tt.expected, ip)
})
}
}
func TestMetricsFriendlyPath(t *testing.T) {
t.Parallel()
tests := []struct {
pattern string
expected string
}{
{
pattern: "/",
expected: "/*",
},
{
pattern: "/exact",
expected: "/exact",
},
{
pattern: "/prefix/",
expected: "/prefix/*",
},
{
pattern: "/chats/{id}/message/{index}",
expected: "/chats/{id}/message/{index}",
},
}
for _, method := range []string{"", "GET ", "HEAD "} {
for _, tt := range tests {
t.Run(tt.pattern, func(t *testing.T) {
t.Parallel()
metricsPath := metricsFriendlyPath(method + tt.pattern)
require.Equal(t, tt.expected, metricsPath)
})
}
}
}

26
pkg/httpx/range.go Normal file
View File

@ -0,0 +1,26 @@
package httpx
import (
"fmt"
"strings"
)
type ByteRange struct {
Start int64 `json:"start"`
End int64 `json:"end"`
}
func FormatRangeHeader(byteRange ByteRange) string {
return fmt.Sprintf("bytes=%d-%d", byteRange.Start, byteRange.End)
}
func FormatMultipartRangeHeader(byteRanges []ByteRange) string {
if len(byteRanges) == 0 {
return ""
}
ranges := []string{}
for _, br := range byteRanges {
ranges = append(ranges, fmt.Sprintf("%d-%d", br.Start, br.End))
}
return "bytes=" + strings.Join(ranges, ", ")
}

35
pkg/httpx/range_test.go Normal file
View File

@ -0,0 +1,35 @@
package httpx
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestFormatRangeHeader(t *testing.T) {
t.Parallel()
br := ByteRange{Start: 10, End: 2000}
val := FormatRangeHeader(br)
require.Equal(t, "bytes=10-2000", val)
}
func TestFormatMultipartRangeHeader(t *testing.T) {
t.Parallel()
brr := []ByteRange{
{
Start: 10,
End: 100,
},
{
Start: 0,
End: 1,
},
}
val := FormatMultipartRangeHeader(brr)
require.Equal(t, "bytes=10-100, 0-1", val)
val = FormatMultipartRangeHeader(nil)
require.Empty(t, val)
}

View File

@ -1,4 +1,4 @@
package mux
package httpx
import (
"bufio"
@ -13,6 +13,7 @@ type ResponseWriter interface {
Error() error
Status() int
Size() int64
SetHandler(handler string)
}
var (
@ -25,6 +26,7 @@ var (
type response struct {
http.ResponseWriter
error error
handler string
status int
size int64
writtenHeader bool
@ -52,11 +54,13 @@ func (r *response) WriteError(statusCode int, err error) {
func (r *response) Flush() {
r.writtenHeader = true
//nolint: errcheck // No method to throw the error.
flusher := r.ResponseWriter.(http.Flusher)
flusher.Flush()
}
func (r *response) Hijack() (net.Conn, *bufio.ReadWriter, error) {
//nolint: errcheck // No method to throw the error.
hijacker := r.ResponseWriter.(http.Hijacker)
return hijacker.Hijack()
}
@ -85,3 +89,7 @@ func (r *response) Error() error {
func (r *response) Size() int64 {
return r.size
}
func (r *response) SetHandler(handler string) {
r.handler = handler
}

View File

@ -0,0 +1,79 @@
package httpx
import (
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestResponseWriter(t *testing.T) {
t.Parallel()
var httpRw http.ResponseWriter = &response{}
_, ok := httpRw.(io.ReaderFrom)
require.True(t, ok)
httpRw = httptest.NewRecorder()
rw := &response{
ResponseWriter: httpRw,
}
require.Equal(t, httpRw, rw.Unwrap())
require.NoError(t, rw.Error())
require.Equal(t, int64(0), rw.Size())
require.Equal(t, http.StatusOK, rw.Status())
rw = &response{
ResponseWriter: httptest.NewRecorder(),
}
rw.WriteHeader(http.StatusNotFound)
require.True(t, rw.writtenHeader)
require.Equal(t, http.StatusNotFound, rw.Status())
rw.WriteHeader(http.StatusBadGateway)
require.Equal(t, http.StatusNotFound, rw.Status())
_, err := rw.Write([]byte("foo"))
require.NoError(t, err)
require.Equal(t, http.StatusNotFound, rw.Status())
rw = &response{
ResponseWriter: httptest.NewRecorder(),
}
err = errors.New("some server error")
rw.WriteError(http.StatusInternalServerError, err)
require.Equal(t, err, rw.Error())
require.Equal(t, http.StatusInternalServerError, rw.Status())
rw = &response{
ResponseWriter: httptest.NewRecorder(),
}
first := "hello world"
n, err := rw.Write([]byte(first))
require.Equal(t, http.StatusOK, rw.Status())
require.NoError(t, err)
require.Equal(t, len(first), n)
require.Equal(t, int64(len(first)), rw.Size())
second := "foo bar"
n, err = rw.Write([]byte(second))
require.NoError(t, err)
require.Equal(t, len(second), n)
require.Equal(t, int64(len(first)+len(second)), rw.Size())
rw = &response{
ResponseWriter: httptest.NewRecorder(),
}
r := strings.NewReader("reader")
readFromN, err := rw.ReadFrom(r)
require.NoError(t, err)
require.Equal(t, r.Size(), readFromN)
require.Equal(t, r.Size(), rw.Size())
rw = &response{
ResponseWriter: httptest.NewRecorder(),
}
rw.SetHandler("foo")
require.Equal(t, "foo", rw.handler)
}

64
pkg/httpx/status.go Normal file
View File

@ -0,0 +1,64 @@
package httpx
import (
"errors"
"fmt"
"io"
"net/http"
"slices"
"strings"
)
type StatusError struct {
Message string
ExpectedCodes []int
StatusCode int
}
func (e *StatusError) Error() string {
expectedCodeStrs := []string{}
for _, expected := range e.ExpectedCodes {
expectedCodeStrs = append(expectedCodeStrs, fmt.Sprintf("%d %s", expected, http.StatusText(expected)))
}
msg := fmt.Sprintf("expected one of the following statuses [%s], but received %d %s", strings.Join(expectedCodeStrs, ", "), e.StatusCode, http.StatusText(e.StatusCode))
if e.Message != "" {
msg += ": " + e.Message
}
return msg
}
func CheckResponseStatus(resp *http.Response, expectedCodes ...int) error {
if len(expectedCodes) == 0 {
return errors.New("expected codes cannot be empty")
}
if slices.Contains(expectedCodes, resp.StatusCode) {
return nil
}
message, messageErr := getErrorMessage(resp)
statusErr := &StatusError{
Message: message,
ExpectedCodes: expectedCodes,
StatusCode: resp.StatusCode,
}
return errors.Join(statusErr, messageErr)
}
func getErrorMessage(resp *http.Response) (string, error) {
if resp.Request.Method == http.MethodHead {
return "", nil
}
contentTypes := []string{
"text/plain",
"text/html",
"application/json",
"application/xml",
}
if !slices.Contains(contentTypes, resp.Header.Get(HeaderContentType)) {
return "", nil
}
b, err := io.ReadAll(io.LimitReader(resp.Body, MaxReadBytes))
if err != nil {
return "", err
}
return string(b), err
}

97
pkg/httpx/status_test.go Normal file
View File

@ -0,0 +1,97 @@
package httpx
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
)
func TestStatusError(t *testing.T) {
t.Parallel()
tests := []struct {
name string
contentType string
body string
expectedError string
requestMethod string
expectedCodes []int
statusCode int
}{
{
name: "status code matches one of expected",
contentType: "text/plain",
body: "Hello World",
statusCode: http.StatusOK,
expectedCodes: []int{http.StatusNotFound, http.StatusOK},
requestMethod: http.MethodGet,
expectedError: "",
},
{
name: "no expected status codes",
contentType: "text/plain",
statusCode: http.StatusOK,
expectedCodes: []int{},
expectedError: "expected codes cannot be empty",
},
{
name: "wrong code with text content and GET request",
contentType: "text/plain",
body: "Hello World",
statusCode: http.StatusNotFound,
expectedCodes: []int{http.StatusOK},
requestMethod: http.MethodGet,
expectedError: "expected one of the following statuses [200 OK], but received 404 Not Found: Hello World",
},
{
name: "wrong code with text content and HEAD request",
contentType: "text/plain",
body: "Hello World",
statusCode: http.StatusNotFound,
expectedCodes: []int{http.StatusOK, http.StatusPartialContent},
requestMethod: http.MethodHead,
expectedError: "expected one of the following statuses [200 OK, 206 Partial Content], but received 404 Not Found",
},
{
name: "wrong code with text content and GET request but octet stream",
contentType: "application/octet-stream",
body: "Hello World",
statusCode: http.StatusNotFound,
expectedCodes: []int{http.StatusOK},
requestMethod: http.MethodGet,
expectedError: "expected one of the following statuses [200 OK], but received 404 Not Found",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
rec := httptest.NewRecorder()
rec.WriteHeader(tt.statusCode)
rec.Header().Set(HeaderContentType, tt.contentType)
rec.Body = bytes.NewBufferString(tt.body)
resp := &http.Response{
StatusCode: tt.statusCode,
Status: http.StatusText(tt.statusCode),
Header: rec.Header(),
Body: io.NopCloser(rec.Body),
Request: &http.Request{
Method: tt.requestMethod,
},
}
err := CheckResponseStatus(resp, tt.expectedCodes...)
if tt.expectedError == "" {
require.NoError(t, err)
} else {
require.EqualError(t, err, tt.expectedError)
}
})
}
}

View File

@ -2,6 +2,8 @@ package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/spegel-org/spegel/pkg/httpx"
)
var (
@ -18,7 +20,7 @@ var (
MirrorRequestsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "spegel_mirror_requests_total",
Help: "Total number of mirror requests.",
}, []string{"registry", "cache", "source"})
}, []string{"registry", "cache"})
ResolveDurHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "spegel_resolve_duration_seconds",
Help: "The duration for router to resolve a peer.",
@ -39,21 +41,6 @@ var (
Name: "spegel_advertised_keys",
Help: "Number of keys advertised to be available.",
}, []string{"registry"})
HttpRequestDurHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Subsystem: "http",
Name: "request_duration_seconds",
Help: "The latency of the HTTP requests.",
}, []string{"handler", "method", "code"})
HttpResponseSizeHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Subsystem: "http",
Name: "response_size_bytes",
Help: "The size of the HTTP responses.",
}, []string{"handler", "method", "code"})
HttpRequestsInflight = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Subsystem: "http",
Name: "requests_inflight",
Help: "The number of inflight requests being handled at the same time.",
}, []string{"handler"})
)
func Register() {
@ -63,7 +50,5 @@ func Register() {
DefaultRegisterer.MustRegister(AdvertisedImageTags)
DefaultRegisterer.MustRegister(AdvertisedImageDigests)
DefaultRegisterer.MustRegister(AdvertisedKeys)
DefaultRegisterer.MustRegister(HttpRequestDurHistogram)
DefaultRegisterer.MustRegister(HttpResponseSizeHistogram)
DefaultRegisterer.MustRegister(HttpRequestsInflight)
httpx.RegisterMetrics(DefaultRegisterer)
}

View File

@ -0,0 +1,11 @@
package metrics
import (
"testing"
)
func TestRegister(t *testing.T) {
t.Parallel()
Register()
}

358
pkg/oci/client.go Normal file
View File

@ -0,0 +1,358 @@
package oci
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"path"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/containerd/containerd/v2/core/images"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spegel-org/spegel/pkg/httpx"
)
const (
HeaderDockerDigest = "Docker-Content-Digest"
)
type FetchConfig struct {
Mirror *url.URL
Header http.Header
}
func (cfg *FetchConfig) Apply(opts ...FetchOption) error {
for _, opt := range opts {
if opt == nil {
continue
}
if err := opt(cfg); err != nil {
return err
}
}
return nil
}
type FetchOption func(cfg *FetchConfig) error
func WithFetchMirror(mirror *url.URL) FetchOption {
return func(cfg *FetchConfig) error {
cfg.Mirror = mirror
return nil
}
}
func WithFetchHeader(header http.Header) FetchOption {
return func(cfg *FetchConfig) error {
cfg.Header = header
return nil
}
}
type Client struct {
hc *http.Client
tc sync.Map
}
func NewClient() *Client {
hc := httpx.BaseClient()
hc.Timeout = 0
return &Client{
hc: hc,
tc: sync.Map{},
}
}
type PullMetric struct {
Digest digest.Digest
ContentType string
ContentLength int64
Duration time.Duration
}
func (c *Client) Pull(ctx context.Context, img Image, opts ...FetchOption) ([]PullMetric, error) {
pullMetrics := []PullMetric{}
queue := []DistributionPath{
{
Kind: DistributionKindManifest,
Name: img.Repository,
Digest: img.Digest,
Tag: img.Tag,
Registry: img.Registry,
},
}
for len(queue) > 0 {
dist := queue[0]
queue = queue[1:]
start := time.Now()
desc, err := func() (ocispec.Descriptor, error) {
rc, desc, err := c.Get(ctx, dist, nil, opts...)
if err != nil {
return ocispec.Descriptor{}, err
}
defer httpx.DrainAndClose(rc)
switch dist.Kind {
case DistributionKindBlob:
// Right now we are just discarding the contents because we do not have a writable store.
_, copyErr := io.Copy(io.Discard, rc)
closeErr := rc.Close()
err := errors.Join(copyErr, closeErr)
if err != nil {
return ocispec.Descriptor{}, err
}
case DistributionKindManifest:
b, readErr := io.ReadAll(rc)
closeErr := rc.Close()
err = errors.Join(readErr, closeErr)
if err != nil {
return ocispec.Descriptor{}, err
}
switch desc.MediaType {
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
var idx ocispec.Index
if err := json.Unmarshal(b, &idx); err != nil {
return ocispec.Descriptor{}, err
}
for _, m := range idx.Manifests {
// TODO: Add platform option.
//nolint: staticcheck // Simplify in the future.
if !(m.Platform.OS == runtime.GOOS && m.Platform.Architecture == runtime.GOARCH) {
continue
}
queue = append(queue, DistributionPath{
Kind: DistributionKindManifest,
Name: dist.Name,
Digest: m.Digest,
Registry: dist.Registry,
})
}
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
var manifest ocispec.Manifest
err := json.Unmarshal(b, &manifest)
if err != nil {
return ocispec.Descriptor{}, err
}
queue = append(queue, DistributionPath{
Kind: DistributionKindBlob,
Name: dist.Name,
Digest: manifest.Config.Digest,
Registry: dist.Registry,
})
for _, layer := range manifest.Layers {
queue = append(queue, DistributionPath{
Kind: DistributionKindBlob,
Name: dist.Name,
Digest: layer.Digest,
Registry: dist.Registry,
})
}
}
}
return desc, nil
}()
if err != nil {
return nil, err
}
metric := PullMetric{
Digest: desc.Digest,
Duration: time.Since(start),
ContentType: desc.MediaType,
ContentLength: desc.Size,
}
pullMetrics = append(pullMetrics, metric)
}
return pullMetrics, nil
}
func (c *Client) Head(ctx context.Context, dist DistributionPath, opts ...FetchOption) (ocispec.Descriptor, error) {
rc, desc, err := c.fetch(ctx, http.MethodHead, dist, nil, opts...)
if err != nil {
return ocispec.Descriptor{}, err
}
defer httpx.DrainAndClose(rc)
return desc, nil
}
func (c *Client) Get(ctx context.Context, dist DistributionPath, brr []httpx.ByteRange, opts ...FetchOption) (io.ReadCloser, ocispec.Descriptor, error) {
rc, desc, err := c.fetch(ctx, http.MethodGet, dist, brr, opts...)
if err != nil {
return nil, ocispec.Descriptor{}, err
}
return rc, desc, nil
}
func (c *Client) fetch(ctx context.Context, method string, dist DistributionPath, brr []httpx.ByteRange, opts ...FetchOption) (io.ReadCloser, ocispec.Descriptor, error) {
cfg := FetchConfig{}
err := cfg.Apply(opts...)
if err != nil {
return nil, ocispec.Descriptor{}, err
}
tcKey := dist.Registry + dist.Name
u := dist.URL()
if cfg.Mirror != nil {
u.Scheme = cfg.Mirror.Scheme
u.Host = cfg.Mirror.Host
u.Path = path.Join(cfg.Mirror.Path, u.Path)
}
if u.Host == "docker.io" {
u.Host = "registry-1.docker.io"
}
for range 2 {
req, err := http.NewRequestWithContext(ctx, method, u.String(), nil)
if err != nil {
return nil, ocispec.Descriptor{}, err
}
httpx.CopyHeader(req.Header, cfg.Header)
req.Header.Set(httpx.HeaderUserAgent, "spegel")
req.Header.Add(httpx.HeaderAccept, "application/vnd.oci.image.manifest.v1+json")
req.Header.Add(httpx.HeaderAccept, "application/vnd.docker.distribution.manifest.v2+json")
req.Header.Add(httpx.HeaderAccept, "application/vnd.oci.image.index.v1+json")
req.Header.Add(httpx.HeaderAccept, "application/vnd.docker.distribution.manifest.list.v2+json")
if len(brr) > 0 {
req.Header.Add(httpx.HeaderRange, httpx.FormatMultipartRangeHeader(brr))
}
token, ok := c.tc.Load(tcKey)
if ok {
//nolint: errcheck // We know it will be a string.
req.Header.Set(httpx.HeaderAuthorization, "Bearer "+token.(string))
}
resp, err := c.hc.Do(req)
if err != nil {
return nil, ocispec.Descriptor{}, err
}
if resp.StatusCode == http.StatusUnauthorized {
c.tc.Delete(tcKey)
wwwAuth := resp.Header.Get(httpx.HeaderWWWAuthenticate)
token, err = getBearerToken(ctx, wwwAuth, c.hc)
if err != nil {
return nil, ocispec.Descriptor{}, err
}
c.tc.Store(tcKey, token)
continue
}
err = httpx.CheckResponseStatus(resp, http.StatusOK, http.StatusPartialContent)
if err != nil {
httpx.DrainAndClose(resp.Body)
return nil, ocispec.Descriptor{}, err
}
// Handle optional headers for blobs.
header := resp.Header.Clone()
if dist.Kind == DistributionKindBlob {
if header.Get(httpx.HeaderContentType) == "" {
header.Set(httpx.HeaderContentType, httpx.ContentTypeBinary)
}
if header.Get(HeaderDockerDigest) == "" {
header.Set(HeaderDockerDigest, dist.Digest.String())
}
}
desc, err := DescriptorFromHeader(header)
if err != nil {
httpx.DrainAndClose(resp.Body)
return nil, ocispec.Descriptor{}, err
}
return resp.Body, desc, nil
}
return nil, ocispec.Descriptor{}, errors.New("could not perform request")
}
func getBearerToken(ctx context.Context, wwwAuth string, client *http.Client) (string, error) {
if !strings.HasPrefix(wwwAuth, "Bearer ") {
return "", errors.New("unsupported auth scheme")
}
params := map[string]string{}
for _, part := range strings.Split(wwwAuth[len("Bearer "):], ",") {
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(kv) == 2 {
params[kv[0]] = strings.Trim(kv[1], `"`)
}
}
authURL, err := url.Parse(params["realm"])
if err != nil {
return "", err
}
q := authURL.Query()
if service, ok := params["service"]; ok {
q.Set("service", service)
}
if scope, ok := params["scope"]; ok {
q.Set("scope", scope)
}
authURL.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, authURL.String(), nil)
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer httpx.DrainAndClose(resp.Body)
err = httpx.CheckResponseStatus(resp, http.StatusOK)
if err != nil {
return "", err
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
tokenResp := struct {
Token string `json:"token"`
}{}
err = json.Unmarshal(b, &tokenResp)
if err != nil {
return "", err
}
return tokenResp.Token, nil
}
func DescriptorFromHeader(header http.Header) (ocispec.Descriptor, error) {
mediaType := header.Get(httpx.HeaderContentType)
if mediaType == "" {
return ocispec.Descriptor{}, errors.New("content type cannot be empty")
}
contentLength := header.Get(httpx.HeaderContentLength)
if contentLength == "" {
return ocispec.Descriptor{}, errors.New("content length cannot be empty")
}
size, err := strconv.ParseInt(contentLength, 10, 64)
if err != nil {
return ocispec.Descriptor{}, err
}
dgst, err := digest.Parse(header.Get(HeaderDockerDigest))
if err != nil {
return ocispec.Descriptor{}, err
}
desc := ocispec.Descriptor{
MediaType: mediaType,
Size: size,
Digest: dgst,
}
return desc, nil
}
func WriteDescriptorToHeader(desc ocispec.Descriptor, header http.Header) {
header.Set(httpx.HeaderContentType, desc.MediaType)
header.Set(httpx.HeaderContentLength, strconv.FormatInt(desc.Size, 10))
header.Set(HeaderDockerDigest, desc.Digest.String())
}

116
pkg/oci/client_test.go Normal file
View File

@ -0,0 +1,116 @@
package oci
import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"cuelabs.dev/go/oci/ociregistry/ocimem"
"cuelabs.dev/go/oci/ociregistry/ociserver"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spegel-org/spegel/pkg/httpx"
"github.com/stretchr/testify/require"
)
func TestClient(t *testing.T) {
t.Parallel()
img := Image{
Repository: "test/image",
Tag: "latest",
}
mem := ocimem.New()
blobs := []ocispec.Descriptor{
{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: digest.Digest("sha256:68b8a989a3e08ddbdb3a0077d35c0d0e59c9ecf23d0634584def8bdbb7d6824f"),
Size: 529,
},
{
MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
Digest: digest.Digest("sha256:3caa2469de2a23cbcc209dd0b9d01cd78ff9a0f88741655991d36baede5b0996"),
Size: 118,
},
}
for _, blob := range blobs {
f, err := os.Open(filepath.Join("testdata", "blobs", "sha256", blob.Digest.Encoded()))
require.NoError(t, err)
_, err = mem.PushBlob(t.Context(), img.Repository, blob, f)
f.Close()
require.NoError(t, err)
}
manifests := []ocispec.Descriptor{
{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: digest.Digest("sha256:b6d6089ca6c395fd563c2084f5dd7bc56a2f5e6a81413558c5be0083287a77e9"),
},
}
for _, manifest := range manifests {
b, err := os.ReadFile(filepath.Join("testdata", "blobs", "sha256", manifest.Digest.Encoded()))
require.NoError(t, err)
_, err = mem.PushManifest(t.Context(), img.Repository, img.Tag, b, manifest.MediaType)
require.NoError(t, err)
}
reg := ociserver.New(mem, nil)
srv := httptest.NewServer(reg)
t.Cleanup(func() {
srv.Close()
})
client := NewClient()
mirror, err := url.Parse(srv.URL)
require.NoError(t, err)
pullResults, err := client.Pull(t.Context(), img, WithFetchMirror(mirror))
require.NoError(t, err)
require.Len(t, pullResults, 3)
dist := DistributionPath{
Kind: DistributionKindBlob,
Name: img.Repository,
Digest: blobs[0].Digest,
}
desc, err := client.Head(t.Context(), dist, WithFetchMirror(mirror))
require.NoError(t, err)
require.Equal(t, dist.Digest, desc.Digest)
require.Equal(t, httpx.ContentTypeBinary, desc.MediaType)
}
func TestDescriptorHeader(t *testing.T) {
t.Parallel()
header := http.Header{}
desc := ocispec.Descriptor{
MediaType: "foo",
Size: 909,
Digest: digest.Digest("sha256:b6d6089ca6c395fd563c2084f5dd7bc56a2f5e6a81413558c5be0083287a77e9"),
}
WriteDescriptorToHeader(desc, header)
require.Equal(t, "foo", header.Get(httpx.HeaderContentType))
require.Equal(t, "909", header.Get(httpx.HeaderContentLength))
require.Equal(t, "sha256:b6d6089ca6c395fd563c2084f5dd7bc56a2f5e6a81413558c5be0083287a77e9", header.Get(HeaderDockerDigest))
headerDesc, err := DescriptorFromHeader(header)
require.NoError(t, err)
require.Equal(t, desc, headerDesc)
header = http.Header{}
_, err = DescriptorFromHeader(header)
require.EqualError(t, err, "content type cannot be empty")
header.Set(httpx.HeaderContentType, "test")
_, err = DescriptorFromHeader(header)
require.EqualError(t, err, "content length cannot be empty")
header.Set(httpx.HeaderContentLength, "wrong")
_, err = DescriptorFromHeader(header)
require.EqualError(t, err, "strconv.ParseInt: parsing \"wrong\": invalid syntax")
header.Set(httpx.HeaderContentLength, "250000")
_, err = DescriptorFromHeader(header)
require.EqualError(t, err, "invalid checksum digest format")
header.Set(HeaderDockerDigest, "foobar")
_, err = DescriptorFromHeader(header)
require.EqualError(t, err, "invalid checksum digest format")
}

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,37 @@
package oci
import (
"context"
"fmt"
iofs "io/fs"
"maps"
"net/url"
"os"
"path/filepath"
"testing"
"github.com/spf13/afero"
"github.com/containerd/containerd/v2/pkg/filters"
"github.com/go-logr/logr"
"github.com/stretchr/testify/require"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
)
func TestNewContainerd(t *testing.T) {
t.Parallel()
c, err := NewContainerd("socket", "namespace", "foo", nil)
require.NoError(t, err)
require.Empty(t, c.contentPath)
require.Nil(t, c.client)
require.Equal(t, "foo", c.registryConfigPath)
c, err = NewContainerd("socket", "namespace", "foo", nil, WithContentPath("local"))
require.NoError(t, err)
require.Equal(t, "local", c.contentPath)
}
func TestVerifyStatusResponse(t *testing.T) {
t.Parallel()
tests := []struct {
name string
configPath string
@ -54,12 +73,13 @@ func TestVerifyStatusResponse(t *testing.T) {
expectedErrMsg: "Containerd discard unpacked layers cannot be enabled",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
resp := &runtimeapi.StatusResponse{
Info: map[string]string{
"config": fmt.Sprintf(`{"registry": {"configPath": "%s"}, "containerd": {"runtimes":{"discardUnpackedLayers": %v}}}`, tt.configPath, tt.discardUnpackedLayers),
"config": fmt.Sprintf(`{"registry": {"configPath": %q}, "containerd": {"discardUnpackedLayers": %v}}`, tt.configPath, tt.discardUnpackedLayers),
},
}
err := verifyStatusResponse(resp, tt.requiredConfigPath)
@ -72,84 +92,278 @@ func TestVerifyStatusResponse(t *testing.T) {
}
}
func TestCreateFilter(t *testing.T) {
func TestVerifyStatusResponseMissingRequired(t *testing.T) {
t.Parallel()
tests := []struct {
name string
expectedListFilter string
expectedEventFilter string
registries []string
name string
config string
expectedErrMsg string
}{
{
name: "only registries",
registries: []string{"https://docker.io", "https://gcr.io"},
expectedListFilter: `name~="^(docker\\.io|gcr\\.io)/"`,
expectedEventFilter: `topic~="/images/create|/images/update|/images/delete",event.name~="^(docker\\.io|gcr\\.io)/"`,
name: "missing discard upacked layers false",
config: `{"registry": {"configPath": "foo"}, "containerd": {"runtimes":{"discardUnpackedLayers": false}}}`,
expectedErrMsg: "field containerd.discardUnpackedLayers missing from config",
},
{
name: "additional image filtes",
registries: []string{"https://docker.io", "https://gcr.io"},
expectedListFilter: `name~="^(docker\\.io|gcr\\.io)/"`,
expectedEventFilter: `topic~="/images/create|/images/update|/images/delete",event.name~="^(docker\\.io|gcr\\.io)/"`,
name: "missing discard upacked layers true",
config: `{"registry": {"configPath": "foo"}, "containerd": {"runtimes":{"discardUnpackedLayers": true}}}`,
expectedErrMsg: "field containerd.discardUnpackedLayers missing from config",
},
{
name: "missing containerd field",
config: `{"registry": {"configPath": "foo"}}`,
expectedErrMsg: "field containerd.discardUnpackedLayers missing from config",
},
{
name: "missing registry field",
config: `{"containerd": {"discardUnpackedLayers": false}}`,
expectedErrMsg: "field registry.configPath missing from config",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
resp := &runtimeapi.StatusResponse{
Info: map[string]string{
"config": tt.config,
},
}
err := verifyStatusResponse(resp, "foo")
require.EqualError(t, err, tt.expectedErrMsg)
})
}
}
func TestBackupConfig(t *testing.T) {
t.Parallel()
log := logr.Discard()
configPath := t.TempDir()
err := backupConfig(log, configPath)
require.NoError(t, err)
ok, err := dirExists(filepath.Join(configPath, "_backup"))
require.NoError(t, err)
require.True(t, ok)
files, err := os.ReadDir(filepath.Join(configPath, "_backup"))
require.NoError(t, err)
require.Empty(t, files)
configPath = t.TempDir()
err = os.WriteFile(filepath.Join(configPath, "test.txt"), []byte("Hello World"), 0o644)
require.NoError(t, err)
err = backupConfig(log, configPath)
require.NoError(t, err)
ok, err = dirExists(filepath.Join(configPath, "_backup"))
require.NoError(t, err)
require.True(t, ok)
files, err = os.ReadDir(filepath.Join(configPath, "_backup"))
require.NoError(t, err)
require.Len(t, files, 1)
}
func TestParseContentRegistries(t *testing.T) {
t.Parallel()
tests := []struct {
name string
labels map[string]string
expected []string
}{
{
name: "no labels",
labels: map[string]string{},
expected: []string{},
},
{
name: "one matching",
labels: map[string]string{
"containerd.io/distribution.source.docker.io": "library/alpine",
},
expected: []string{"docker.io"},
},
{
name: "multiple matching",
labels: map[string]string{
"containerd.io/distribution.source.example.com": "foo",
"containerd.io/distribution.source.ghcr.io": "spegel-org/spegel",
},
expected: []string{"ghcr.io", "example.com"},
},
}
for _, tt := range tests {
t.Run(t.Name(), func(t *testing.T) {
t.Parallel()
registries := parseContentRegistries(tt.labels)
require.ElementsMatch(t, tt.expected, registries)
})
}
}
func TestFeaturesForVersion(t *testing.T) {
t.Parallel()
tests := []struct {
version string
expectedString string
expectedFeatures []Feature
}{
{
version: "v2.0.2",
expectedFeatures: []Feature{},
expectedString: "",
},
{
version: "2.1.0",
expectedFeatures: []Feature{FeatureContentEvent},
expectedString: "ContentEvent",
},
{
version: "v1.7.27",
expectedFeatures: []Feature{FeatureConfigCheck},
expectedString: "ConfigCheck",
},
{
version: "1.6.0",
expectedFeatures: []Feature{FeatureConfigCheck},
expectedString: "ConfigCheck",
},
}
for _, tt := range tests {
// Testing with a suffix is important as some Linux distributions will modify the version
// with a non Semver compliant modification. Even if the version is supposed to comply with
// semver that may not always be the case.
for _, suffix := range []string{"", "~ds1"} {
version := tt.version + suffix
t.Run(version, func(t *testing.T) {
t.Parallel()
feats, err := featuresForVersion(tt.version)
require.NoError(t, err)
for _, feat := range tt.expectedFeatures {
ok := feats.Has(feat)
require.True(t, ok)
}
require.Equal(t, tt.expectedString, feats.String())
})
}
}
}
func TestCreateFilter(t *testing.T) {
t.Parallel()
tests := []struct {
name string
expectedImageFilter []string
expectedEventFilter []string
expectedContentFilter []string
registries []string
}{
{
name: "with registry filtering",
registries: []string{"https://docker.io", "https://gcr.io"},
expectedImageFilter: []string{`name~="^(docker\\.io|gcr\\.io)/"`},
expectedEventFilter: []string{`topic~="/images/create|/images/delete",event.name~="^(docker\\.io|gcr\\.io)/"`, `topic~="/content/create"`},
expectedContentFilter: []string{`labels."containerd.io/distribution.source.docker.io"~="^."`, `labels."containerd.io/distribution.source.gcr.io"~="^."`},
},
{
name: "without registry filtering",
registries: []string{},
expectedImageFilter: []string{`name~="^.+/"`},
expectedEventFilter: []string{`topic~="/images/create|/images/delete",event.name~="^.+/"`, `topic~="/content/create"`},
expectedContentFilter: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
listFilter, eventFilter := createFilters(stringListToUrlList(t, tt.registries))
require.Equal(t, tt.expectedListFilter, listFilter)
t.Parallel()
imageFilter, eventFilter, contentFilter := createFilters(stringListToUrlList(t, tt.registries))
require.Equal(t, tt.expectedImageFilter, imageFilter)
_, err := filters.ParseAll(imageFilter...)
require.NoError(t, err)
require.Equal(t, tt.expectedEventFilter, eventFilter)
_, err = filters.ParseAll(eventFilter...)
require.NoError(t, err)
require.Equal(t, tt.expectedContentFilter, contentFilter)
_, err = filters.ParseAll(contentFilter...)
require.NoError(t, err)
})
}
}
func TestMirrorConfiguration(t *testing.T) {
registryConfigPath := "/etc/containerd/certs.d"
t.Parallel()
tests := []struct {
existingFiles map[string]string
expectedFiles map[string]string
name string
username string
password string
registries []url.URL
mirrors []url.URL
resolveTags bool
createConfigPathDir bool
appendToBackup bool
prependExisting bool
}{
{
name: "multiple mirros",
resolveTags: true,
registries: stringListToUrlList(t, []string{"http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000", "http://127.0.0.1:5001"}),
name: "multiple mirrors",
resolveTags: true,
registries: stringListToUrlList(t, []string{"http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000", "http://127.0.0.2:5000", "http://127.0.0.1:5001"}),
prependExisting: false,
expectedFiles: map[string]string{
"/etc/containerd/certs.d/foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host]
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
dial_timeout = '200ms'
[host.'http://127.0.0.2:5000']
capabilities = ['pull', 'resolve']
dial_timeout = '200ms'
[host.'http://127.0.0.1:5001']
capabilities = ['pull', 'resolve']
`,
dial_timeout = '200ms'`,
},
},
{
name: "resolve tags disabled",
resolveTags: false,
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
name: "_default registry mirrors",
resolveTags: true,
registries: stringListToUrlList(t, []string{}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
prependExisting: false,
expectedFiles: map[string]string{
"/etc/containerd/certs.d/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
"_default/hosts.toml": `[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
dial_timeout = '200ms'`,
},
},
{
name: "resolve tags disabled",
resolveTags: false,
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
prependExisting: false,
expectedFiles: map[string]string{
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host]
[host.'http://127.0.0.1:5000']
capabilities = ['pull']
`,
"/etc/containerd/certs.d/foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
dial_timeout = '200ms'`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host]
[host.'http://127.0.0.1:5000']
capabilities = ['pull']
`,
dial_timeout = '200ms'`,
},
},
{
@ -158,19 +372,18 @@ capabilities = ['pull']
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: false,
prependExisting: false,
expectedFiles: map[string]string{
"/etc/containerd/certs.d/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host]
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
`,
"/etc/containerd/certs.d/foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
dial_timeout = '200ms'`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host]
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
`,
dial_timeout = '200ms'`,
},
},
{
@ -179,19 +392,18 @@ capabilities = ['pull', 'resolve']
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: true,
prependExisting: false,
expectedFiles: map[string]string{
"/etc/containerd/certs.d/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host]
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
`,
"/etc/containerd/certs.d/foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
dial_timeout = '200ms'`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host]
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
`,
dial_timeout = '200ms'`,
},
},
{
@ -200,25 +412,24 @@ capabilities = ['pull', 'resolve']
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: true,
prependExisting: false,
existingFiles: map[string]string{
"/etc/containerd/certs.d/docker.io/hosts.toml": "Hello World",
"/etc/containerd/certs.d/ghcr.io/hosts.toml": "Foo Bar",
"docker.io/hosts.toml": "hello = 'world'",
"ghcr.io/hosts.toml": "foo = 'bar'",
},
expectedFiles: map[string]string{
"/etc/containerd/certs.d/_backup/docker.io/hosts.toml": "Hello World",
"/etc/containerd/certs.d/_backup/ghcr.io/hosts.toml": "Foo Bar",
"/etc/containerd/certs.d/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
"_backup/docker.io/hosts.toml": "hello = 'world'",
"_backup/ghcr.io/hosts.toml": "foo = 'bar'",
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host]
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
`,
"/etc/containerd/certs.d/foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
dial_timeout = '200ms'`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host]
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
`,
dial_timeout = '200ms'`,
},
},
{
@ -227,53 +438,70 @@ capabilities = ['pull', 'resolve']
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: true,
prependExisting: false,
existingFiles: map[string]string{
"/etc/containerd/certs.d/_backup/docker.io/hosts.toml": "Hello World",
"/etc/containerd/certs.d/_backup/ghcr.io/hosts.toml": "Foo Bar",
"/etc/containerd/certs.d/test.txt": "test",
"/etc/containerd/certs.d/foo": "bar",
"_backup/docker.io/hosts.toml": "hello = 'world'",
"_backup/ghcr.io/hosts.toml": "foo = 'bar'",
"test.txt": "test",
"foo": "bar",
},
expectedFiles: map[string]string{
"/etc/containerd/certs.d/_backup/docker.io/hosts.toml": "Hello World",
"/etc/containerd/certs.d/_backup/ghcr.io/hosts.toml": "Foo Bar",
"/etc/containerd/certs.d/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
"_backup/docker.io/hosts.toml": "hello = 'world'",
"_backup/ghcr.io/hosts.toml": "foo = 'bar'",
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host]
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
`,
"/etc/containerd/certs.d/foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
dial_timeout = '200ms'`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host]
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
`,
dial_timeout = '200ms'`,
},
},
{
name: "append to existing configuration",
name: "prepend to existing configuration",
resolveTags: true,
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: true,
appendToBackup: true,
prependExisting: true,
existingFiles: map[string]string{
"/etc/containerd/certs.d/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host]
[host.'http://example.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
[host.'http://example.com:30021']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
`,
capabilities = ['pull', 'resolve']
[host.'http://bar.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']`,
},
expectedFiles: map[string]string{
"/etc/containerd/certs.d/_backup/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
"_backup/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://example.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
[host.'http://example.com:30021']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
capabilities = ['pull', 'resolve']
[host.'http://bar.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']`,
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
dial_timeout = '200ms'
[host]
[host.'http://example.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
@ -281,87 +509,267 @@ client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
[host.'http://example.com:30021']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
`,
"/etc/containerd/certs.d/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host]
[host.'http://bar.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
dial_timeout = '200ms'`,
},
},
{
name: "prepend existing disabled",
resolveTags: true,
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: true,
prependExisting: false,
existingFiles: map[string]string{
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://example.com:30020']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
[host.'http://example.com:30021']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
capabilities = ['pull', 'resolve']
`,
"/etc/containerd/certs.d/foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host]
[host.'http://bar.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']`,
},
expectedFiles: map[string]string{
"_backup/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://example.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
[host.'http://example.com:30021']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
capabilities = ['pull', 'resolve']
[host.'http://bar.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']`,
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
`,
dial_timeout = '200ms'`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
dial_timeout = '200ms'`,
},
},
{
name: "with basic authentication",
resolveTags: true,
registries: stringListToUrlList(t, []string{"http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000", "http://127.0.0.1:5001"}),
prependExisting: false,
username: "hello",
password: "world",
expectedFiles: map[string]string{
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
dial_timeout = '200ms'
[host.'http://127.0.0.1:5000'.header]
Authorization = 'Basic aGVsbG86d29ybGQ='
[host.'http://127.0.0.1:5001']
capabilities = ['pull', 'resolve']
dial_timeout = '200ms'
[host.'http://127.0.0.1:5001'.header]
Authorization = 'Basic aGVsbG86d29ybGQ='`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fs := afero.NewMemMapFs()
t.Parallel()
registryConfigPath := filepath.Join(t.TempDir(), "etc", "containerd", "certs.d")
if tt.createConfigPathDir {
err := fs.Mkdir(registryConfigPath, 0755)
err := os.MkdirAll(registryConfigPath, 0o755)
require.NoError(t, err)
}
for k, v := range tt.existingFiles {
err := afero.WriteFile(fs, k, []byte(v), 0644)
path := filepath.Join(registryConfigPath, k)
err := os.MkdirAll(filepath.Dir(path), 0o755)
require.NoError(t, err)
err = os.WriteFile(path, []byte(v), 0o644)
require.NoError(t, err)
}
err := AddMirrorConfiguration(context.TODO(), fs, registryConfigPath, tt.registries, tt.mirrors, tt.resolveTags, tt.appendToBackup)
err := AddMirrorConfiguration(t.Context(), registryConfigPath, tt.registries, tt.mirrors, tt.resolveTags, tt.prependExisting, tt.username, tt.password)
require.NoError(t, err)
if len(tt.existingFiles) == 0 {
ok, err := afero.DirExists(fs, "/etc/containerd/certs.d/_backup")
require.NoError(t, err)
require.False(t, ok)
}
err = afero.Walk(fs, registryConfigPath, func(path string, fi iofs.FileInfo, _ error) error {
ok, err := dirExists(filepath.Join(registryConfigPath, "_backup"))
require.NoError(t, err)
require.True(t, ok)
seenExpectedFiles := maps.Clone(tt.expectedFiles)
err = filepath.Walk(registryConfigPath, func(path string, fi iofs.FileInfo, _ error) error {
if fi.IsDir() {
return nil
}
expectedContent, ok := tt.expectedFiles[path]
require.True(t, ok, path)
b, err := afero.ReadFile(fs, path)
relPath, err := filepath.Rel(registryConfigPath, path)
require.NoError(t, err)
expectedContent, ok := tt.expectedFiles[relPath]
require.True(t, ok)
delete(seenExpectedFiles, relPath)
b, err := os.ReadFile(path)
require.NoError(t, err)
require.Equal(t, expectedContent, string(b))
return nil
})
require.NoError(t, err)
require.Empty(t, seenExpectedFiles)
})
}
}
func TestMirrorConfigurationInvalidMirrorURL(t *testing.T) {
fs := afero.NewMemMapFs()
t.Parallel()
configPath := filepath.Join(t.TempDir(), "etc", "containerd", "certs.d")
mirrors := stringListToUrlList(t, []string{"http://127.0.0.1:5000"})
registries := stringListToUrlList(t, []string{"ftp://docker.io"})
err := AddMirrorConfiguration(context.TODO(), fs, "/etc/containerd/certs.d", registries, mirrors, true, false)
err := AddMirrorConfiguration(t.Context(), configPath, registries, mirrors, true, false, "", "")
require.EqualError(t, err, "invalid registry url scheme must be http or https: ftp://docker.io")
registries = stringListToUrlList(t, []string{"https://docker.io/foo/bar"})
err = AddMirrorConfiguration(context.TODO(), fs, "/etc/containerd/certs.d", registries, mirrors, true, false)
err = AddMirrorConfiguration(t.Context(), configPath, registries, mirrors, true, false, "", "")
require.EqualError(t, err, "invalid registry url path has to be empty: https://docker.io/foo/bar")
registries = stringListToUrlList(t, []string{"https://docker.io?foo=bar"})
err = AddMirrorConfiguration(context.TODO(), fs, "/etc/containerd/certs.d", registries, mirrors, true, false)
err = AddMirrorConfiguration(t.Context(), configPath, registries, mirrors, true, false, "", "")
require.EqualError(t, err, "invalid registry url query has to be empty: https://docker.io?foo=bar")
registries = stringListToUrlList(t, []string{"https://foo@docker.io"})
err = AddMirrorConfiguration(context.TODO(), fs, "/etc/containerd/certs.d", registries, mirrors, true, false)
err = AddMirrorConfiguration(t.Context(), configPath, registries, mirrors, true, false, "", "")
require.EqualError(t, err, "invalid registry url user has to be empty: https://foo@docker.io")
}
func TestExistingHosts(t *testing.T) {
t.Parallel()
configPath := t.TempDir()
u, err := url.Parse("https://ghcr.io")
require.NoError(t, err)
eh, err := existingHosts(configPath, *u)
require.NoError(t, err)
require.Empty(t, eh)
tomlHosts := `server = "https://registry-1.docker.io"
[host."https://mirror.registry"]
capabilities = ["pull"]
ca = "/etc/certs/mirror.pem"
skip_verify = false
[host."https://mirror.registry".header]
x-custom-2 = ["value1", "value2"]
[host]
[host."https://mirror-bak.registry/us"]
capabilities = ["pull"]
skip_verify = true
[host."http://mirror.registry"]
capabilities = ["pull"]
[host."https://test-3.registry"]
client = ["/etc/certs/client-1.pem", "/etc/certs/client-2.pem"]
[host."https://test-2.registry".header]
x-custom-2 = ["foo"]
[host."https://test-1.registry"]
capabilities = ["pull", "resolve", "push"]
ca = ["/etc/certs/test-1-ca.pem", "/etc/certs/special.pem"]
client = [["/etc/certs/client.cert", "/etc/certs/client.key"],["/etc/certs/client.pem", ""]]
[host."https://test-2.registry"]
client = "/etc/certs/client.pem"
[host."https://non-compliant-mirror.registry/v2/upstream"]
capabilities = ["pull"]
override_path = true`
err = os.MkdirAll(filepath.Join(configPath, backupDir, u.Host), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(configPath, backupDir, u.Host, "hosts.toml"), []byte(tomlHosts), 0o644)
require.NoError(t, err)
eh, err = existingHosts(configPath, *u)
require.NoError(t, err)
expected := `[host.'https://mirror.registry']
ca = '/etc/certs/mirror.pem'
capabilities = ['pull']
skip_verify = false
[host.'https://mirror.registry'.header]
x-custom-2 = ['value1', 'value2']
[host.'https://mirror-bak.registry/us']
capabilities = ['pull']
skip_verify = true
[host.'http://mirror.registry']
capabilities = ['pull']
[host.'https://test-3.registry']
client = ['/etc/certs/client-1.pem', '/etc/certs/client-2.pem']
[host.'https://test-1.registry']
ca = ['/etc/certs/test-1-ca.pem', '/etc/certs/special.pem']
capabilities = ['pull', 'resolve', 'push']
client = [['/etc/certs/client.cert', '/etc/certs/client.key'], ['/etc/certs/client.pem', '']]
[host.'https://test-2.registry']
client = '/etc/certs/client.pem'
[host.'https://test-2.registry'.header]
x-custom-2 = ['foo']
[host.'https://non-compliant-mirror.registry/v2/upstream']
capabilities = ['pull']
override_path = true`
require.Equal(t, expected, eh)
}
func TestCleanupMirrorConfiguration(t *testing.T) {
t.Parallel()
configPath := filepath.Join(t.TempDir(), "certs.d")
err := os.MkdirAll(filepath.Join(configPath, "_backup"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(configPath, backupDir, "data.txt"), []byte("hello world"), 0o644)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(configPath, "foo.bin"), []byte("hello world"), 0o644)
require.NoError(t, err)
err = os.MkdirAll(filepath.Join(configPath, "docker.io"), 0o755)
require.NoError(t, err)
for range 2 {
err = CleanupMirrorConfiguration(t.Context(), configPath)
require.NoError(t, err)
files, err := os.ReadDir(configPath)
require.NoError(t, err)
require.Len(t, files, 1)
require.Equal(t, "data.txt", files[0].Name())
}
}
func stringListToUrlList(t *testing.T, list []string) []url.URL {
t.Helper()
urls := []url.URL{}
for _, item := range list {
u, err := url.Parse(item)

111
pkg/oci/distribution.go Normal file
View File

@ -0,0 +1,111 @@
package oci
import (
"errors"
"fmt"
"net/url"
"regexp"
"github.com/opencontainers/go-digest"
)
var (
nameRegex = regexp.MustCompile(`([a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*)`)
tagRegex = regexp.MustCompile(`([a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})`)
manifestRegexTag = regexp.MustCompile(`/v2/` + nameRegex.String() + `/manifests/` + tagRegex.String() + `$`)
manifestRegexDigest = regexp.MustCompile(`/v2/` + nameRegex.String() + `/manifests/(.*)`)
blobsRegexDigest = regexp.MustCompile(`/v2/` + nameRegex.String() + `/blobs/(.*)`)
)
// DistributionKind represents the kind of content.
type DistributionKind string
const (
DistributionKindManifest = "manifests"
DistributionKindBlob = "blobs"
)
// DistributionPath contains the individual parameters from a OCI distribution spec request.
type DistributionPath struct {
Kind DistributionKind
Name string
Digest digest.Digest
Tag string
Registry string
}
// Reference returns the digest if set or alternatively if not the full image reference with the tag.
func (d DistributionPath) Reference() string {
if d.Digest != "" {
return d.Digest.String()
}
return fmt.Sprintf("%s/%s:%s", d.Registry, d.Name, d.Tag)
}
// IsLatestTag returns true if the tag has the value latest.
func (d DistributionPath) IsLatestTag() bool {
return d.Tag == "latest"
}
// URL returns the reconstructed URL containing the path and query parameters.
func (d DistributionPath) URL() *url.URL {
ref := d.Digest.String()
if ref == "" {
ref = d.Tag
}
return &url.URL{
Scheme: "https",
Host: d.Registry,
Path: fmt.Sprintf("/v2/%s/%s/%s", d.Name, d.Kind, ref),
RawQuery: fmt.Sprintf("ns=%s", d.Registry),
}
}
// ParseDistributionPath gets the parameters from a URL which conforms with the OCI distribution spec.
// It returns a distribution path which contains all the individual parameters.
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md
func ParseDistributionPath(u *url.URL) (DistributionPath, error) {
registry := u.Query().Get("ns")
comps := manifestRegexTag.FindStringSubmatch(u.Path)
if len(comps) == 6 {
if registry == "" {
return DistributionPath{}, errors.New("registry parameter needs to be set for tag references")
}
dist := DistributionPath{
Kind: DistributionKindManifest,
Name: comps[1],
Tag: comps[5],
Registry: registry,
}
return dist, nil
}
comps = manifestRegexDigest.FindStringSubmatch(u.Path)
if len(comps) == 6 {
dgst, err := digest.Parse(comps[5])
if err != nil {
return DistributionPath{}, err
}
dist := DistributionPath{
Kind: DistributionKindManifest,
Name: comps[1],
Digest: dgst,
Registry: registry,
}
return dist, nil
}
comps = blobsRegexDigest.FindStringSubmatch(u.Path)
if len(comps) == 6 {
dgst, err := digest.Parse(comps[5])
if err != nil {
return DistributionPath{}, err
}
dist := DistributionPath{
Kind: DistributionKindBlob,
Name: comps[1],
Digest: dgst,
Registry: registry,
}
return dist, nil
}
return DistributionPath{}, errors.New("distribution path could not be parsed")
}

View File

@ -0,0 +1,149 @@
package oci
import (
"fmt"
"net/url"
"testing"
"github.com/opencontainers/go-digest"
"github.com/stretchr/testify/require"
)
func TestParseDistributionPath(t *testing.T) {
t.Parallel()
tests := []struct {
name string
registry string
path string
expectedName string
expectedDgst digest.Digest
expectedTag string
expectedRef string
expectedKind DistributionKind
execptedIsLatestTag bool
}{
{
name: "manifest tag",
registry: "example.com",
path: "/v2/foo/bar/manifests/hello-world",
expectedName: "foo/bar",
expectedDgst: "",
expectedTag: "hello-world",
expectedRef: "example.com/foo/bar:hello-world",
expectedKind: DistributionKindManifest,
execptedIsLatestTag: false,
},
{
name: "manifest with latest tag",
registry: "example.com",
path: "/v2/test/manifests/latest",
expectedName: "test",
expectedDgst: "",
expectedTag: "latest",
expectedRef: "example.com/test:latest",
expectedKind: DistributionKindManifest,
execptedIsLatestTag: true,
},
{
name: "manifest digest",
registry: "docker.io",
path: "/v2/library/nginx/manifests/sha256:0a404ca8e119d061cdb2dceee824c914cdc69b31bc7b5956ef5a520436a80d39",
expectedName: "library/nginx",
expectedDgst: digest.Digest("sha256:0a404ca8e119d061cdb2dceee824c914cdc69b31bc7b5956ef5a520436a80d39"),
expectedTag: "",
expectedRef: "sha256:0a404ca8e119d061cdb2dceee824c914cdc69b31bc7b5956ef5a520436a80d39",
expectedKind: DistributionKindManifest,
execptedIsLatestTag: false,
},
{
name: "blob digest",
registry: "docker.io",
path: "/v2/library/nginx/blobs/sha256:295c7be079025306c4f1d65997fcf7adb411c88f139ad1d34b537164aa060369",
expectedName: "library/nginx",
expectedDgst: digest.Digest("sha256:295c7be079025306c4f1d65997fcf7adb411c88f139ad1d34b537164aa060369"),
expectedTag: "",
expectedRef: "sha256:295c7be079025306c4f1d65997fcf7adb411c88f139ad1d34b537164aa060369",
expectedKind: DistributionKindBlob,
execptedIsLatestTag: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
u := &url.URL{
Path: tt.path,
RawQuery: fmt.Sprintf("ns=%s", tt.registry),
}
dist, err := ParseDistributionPath(u)
require.NoError(t, err)
require.Equal(t, tt.expectedName, dist.Name)
require.Equal(t, tt.expectedDgst, dist.Digest)
require.Equal(t, tt.expectedTag, dist.Tag)
require.Equal(t, tt.expectedRef, dist.Reference())
require.Equal(t, tt.expectedKind, dist.Kind)
require.Equal(t, tt.registry, dist.Registry)
require.Equal(t, tt.path, dist.URL().Path)
require.Equal(t, tt.registry, dist.URL().Query().Get("ns"))
require.Equal(t, tt.execptedIsLatestTag, dist.IsLatestTag())
})
}
}
func TestParseDistributionPathErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
url *url.URL
expectedError string
}{
{
name: "invalid path",
url: &url.URL{
Path: "/v2/spegel-org/spegel/v0.0.1",
RawQuery: "ns=example.com",
},
expectedError: "distribution path could not be parsed",
},
{
name: "blob with tag reference",
url: &url.URL{
Path: "/v2/spegel-org/spegel/blobs/v0.0.1",
RawQuery: "ns=example.com",
},
expectedError: "invalid checksum digest format",
},
{
name: "blob with invalid digest",
url: &url.URL{
Path: "/v2/spegel-org/spegel/blobs/sha256:123",
RawQuery: "ns=example.com",
},
expectedError: "invalid checksum digest length",
},
{
name: "manifest tag with missing registry",
url: &url.URL{
Path: "/v2/spegel-org/spegel/manifests/v0.0.1",
},
expectedError: "registry parameter needs to be set for tag references",
},
{
name: "manifest with invalid digest",
url: &url.URL{
Path: "/v2/spegel-org/spegel/manifests/sha253:foobar",
},
expectedError: "unsupported digest algorithm",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, err := ParseDistributionPath(tt.url)
require.EqualError(t, err, tt.expectedError)
})
}
}

View File

@ -1,6 +1,7 @@
package oci
import (
"errors"
"fmt"
"net/url"
"regexp"
@ -10,42 +11,25 @@ import (
)
type Image struct {
Name string
Registry string
Repository string
Tag string
Digest digest.Digest
}
type EventType string
const (
CreateEvent EventType = "CREATE"
UpdateEvent EventType = "UPDATE"
DeleteEvent EventType = "DELETE"
UnknownEvent EventType = ""
)
type ImageEvent struct {
Image Image
Type EventType
}
func NewImage(name, registry, repository, tag string, dgst digest.Digest) (Image, error) {
if name == "" {
return Image{}, fmt.Errorf("image needs to contain a name")
}
func NewImage(registry, repository, tag string, dgst digest.Digest) (Image, error) {
if registry == "" {
return Image{}, fmt.Errorf("image needs to contain a registry")
return Image{}, errors.New("image needs to contain a registry")
}
if repository == "" {
return Image{}, fmt.Errorf("image needs to repository a digest")
return Image{}, errors.New("image needs to contain a repository")
}
if dgst == "" {
return Image{}, fmt.Errorf("image needs to contain a digest")
if dgst != "" {
if err := dgst.Validate(); err != nil {
return Image{}, err
}
}
return Image{
Name: name,
Registry: registry,
Repository: repository,
Tag: tag,
@ -60,9 +44,13 @@ func (i Image) IsLatestTag() bool {
func (i Image) String() string {
tag := ""
if i.Tag != "" {
tag = fmt.Sprintf(":%s", i.Tag)
tag = ":" + i.Tag
}
return fmt.Sprintf("%s/%s%s@%s", i.Registry, i.Repository, tag, i.Digest.String())
digest := ""
if i.Digest != "" {
digest = "@" + i.Digest.String()
}
return fmt.Sprintf("%s/%s%s%s", i.Registry, i.Repository, tag, digest)
}
func (i Image) TagName() (string, bool) {
@ -74,19 +62,19 @@ func (i Image) TagName() (string, bool) {
var splitRe = regexp.MustCompile(`[:@]`)
func Parse(s string, extraDgst digest.Digest) (Image, error) {
func ParseImage(s string) (Image, error) {
if strings.Contains(s, "://") {
return Image{}, fmt.Errorf("invalid reference")
return Image{}, errors.New("invalid reference")
}
u, err := url.Parse("dummy://" + s)
if err != nil {
return Image{}, err
}
if u.Scheme != "dummy" {
return Image{}, fmt.Errorf("invalid reference")
return Image{}, errors.New("invalid reference")
}
if u.Host == "" {
return Image{}, fmt.Errorf("hostname required")
return Image{}, errors.New("hostname required")
}
var object string
if idx := splitRe.FindStringIndex(u.Path); idx != nil {
@ -102,19 +90,33 @@ func Parse(s string, extraDgst digest.Digest) (Image, error) {
tag, _, _ = strings.Cut(tag, "@")
repository := strings.TrimPrefix(u.Path, "/")
if dgst == "" {
dgst = extraDgst
}
if extraDgst != "" && dgst != extraDgst {
return Image{}, fmt.Errorf("invalid digest set does not match parsed digest: %v %v", s, dgst)
}
img, err := NewImage(s, u.Host, repository, tag, dgst)
img, err := NewImage(u.Host, repository, tag, dgst)
if err != nil {
return Image{}, err
}
return img, nil
}
func ParseImageRequireDigest(s string, dgst digest.Digest) (Image, error) {
img, err := ParseImage(s)
if err != nil {
return Image{}, err
}
if img.Digest != "" && dgst == "" {
return img, nil
}
if img.Digest == "" && dgst == "" {
return Image{}, errors.New("image needs to contain a digest")
}
if img.Digest == "" && dgst != "" {
return NewImage(img.Registry, img.Repository, img.Tag, dgst)
}
if img.Digest != dgst {
return Image{}, fmt.Errorf("invalid digest set does not match parsed digest: %v %v", s, img.Digest)
}
return img, nil
}
func splitObject(obj string) (tag string, dgst digest.Digest) {
parts := strings.SplitAfterN(obj, "@", 2)
if len(parts) < 2 {

View File

@ -8,13 +8,17 @@ import (
"github.com/stretchr/testify/require"
)
func TestParseImage(t *testing.T) {
func TestParseImageRequireDigest(t *testing.T) {
t.Parallel()
tests := []struct {
name string
image string
expectedRepository string
expectedTag string
expectedString string
expectedDigest digest.Digest
expectedIsLatest bool
digestInImage bool
}{
{
@ -24,6 +28,8 @@ func TestParseImage(t *testing.T) {
expectedRepository: "library/ubuntu",
expectedTag: "latest",
expectedDigest: digest.Digest("sha256:c0669ef34cdc14332c0f1ab0c2c01acb91d96014b172f1a76f3a39e63d1f0bda"),
expectedIsLatest: true,
expectedString: "library/ubuntu:latest@sha256:c0669ef34cdc14332c0f1ab0c2c01acb91d96014b172f1a76f3a39e63d1f0bda",
},
{
name: "Only tag",
@ -32,6 +38,8 @@ func TestParseImage(t *testing.T) {
expectedRepository: "library/alpine",
expectedTag: "3.18.0",
expectedDigest: digest.Digest("sha256:c0669ef34cdc14332c0f1ab0c2c01acb91d96014b172f1a76f3a39e63d1f0bda"),
expectedIsLatest: false,
expectedString: "library/alpine:3.18.0@sha256:c0669ef34cdc14332c0f1ab0c2c01acb91d96014b172f1a76f3a39e63d1f0bda",
},
{
name: "Tag and digest",
@ -40,6 +48,8 @@ func TestParseImage(t *testing.T) {
expectedRepository: "jetstack/cert-manager-controller",
expectedTag: "3.18.0",
expectedDigest: digest.Digest("sha256:c0669ef34cdc14332c0f1ab0c2c01acb91d96014b172f1a76f3a39e63d1f0bda"),
expectedIsLatest: false,
expectedString: "jetstack/cert-manager-controller:3.18.0@sha256:c0669ef34cdc14332c0f1ab0c2c01acb91d96014b172f1a76f3a39e63d1f0bda",
},
{
name: "Only digest",
@ -48,14 +58,27 @@ func TestParseImage(t *testing.T) {
expectedRepository: "fluxcd/helm-controller",
expectedTag: "",
expectedDigest: digest.Digest("sha256:c0669ef34cdc14332c0f1ab0c2c01acb91d96014b172f1a76f3a39e63d1f0bda"),
expectedIsLatest: false,
expectedString: "fluxcd/helm-controller@sha256:c0669ef34cdc14332c0f1ab0c2c01acb91d96014b172f1a76f3a39e63d1f0bda",
},
{
name: "Digest only in extra digest",
image: "foo/bar",
digestInImage: false,
expectedRepository: "foo/bar",
expectedDigest: digest.Digest("sha256:c0669ef34cdc14332c0f1ab0c2c01acb91d96014b172f1a76f3a39e63d1f0bda"),
expectedIsLatest: false,
expectedString: "foo/bar@sha256:c0669ef34cdc14332c0f1ab0c2c01acb91d96014b172f1a76f3a39e63d1f0bda",
},
}
registries := []string{"docker.io", "quay.io", "ghcr.com", "127.0.0.1"}
for _, registry := range registries {
for _, tt := range tests {
t.Run(fmt.Sprintf("%s_%s", tt.name, registry), func(t *testing.T) {
t.Parallel()
for _, extraDgst := range []string{tt.expectedDigest.String(), ""} {
img, err := Parse(fmt.Sprintf("%s/%s", registry, tt.image), digest.Digest(extraDgst))
img, err := ParseImageRequireDigest(fmt.Sprintf("%s/%s", registry, tt.image), digest.Digest(extraDgst))
if !tt.digestInImage && extraDgst == "" {
require.EqualError(t, err, "image needs to contain a digest")
continue
@ -65,19 +88,109 @@ func TestParseImage(t *testing.T) {
require.Equal(t, tt.expectedRepository, img.Repository)
require.Equal(t, tt.expectedTag, img.Tag)
require.Equal(t, tt.expectedDigest, img.Digest)
require.Equal(t, tt.expectedIsLatest, img.IsLatestTag())
tagName, ok := img.TagName()
if tt.expectedTag == "" {
require.False(t, ok)
require.Empty(t, tagName)
} else {
require.True(t, ok)
require.Equal(t, registry+"/"+tt.expectedRepository+":"+tt.expectedTag, tagName)
}
require.Equal(t, fmt.Sprintf("%s/%s", registry, tt.expectedString), img.String())
}
})
}
}
}
func TestParseImageDigestDoesNotMatch(t *testing.T) {
_, err := Parse("quay.io/jetstack/cert-manager-webhook@sha256:13fd9eaadb4e491ef0e1d82de60cb199f5ad2ea5a3f8e0c19fdf31d91175b9cb", digest.Digest("sha256:ec4306b243d98cce7c3b1f994f2dae660059ef521b2b24588cfdc950bd816d4c"))
require.EqualError(t, err, "invalid digest set does not match parsed digest: quay.io/jetstack/cert-manager-webhook@sha256:13fd9eaadb4e491ef0e1d82de60cb199f5ad2ea5a3f8e0c19fdf31d91175b9cb sha256:13fd9eaadb4e491ef0e1d82de60cb199f5ad2ea5a3f8e0c19fdf31d91175b9cb")
func TestParseImageRequireDigestErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
s string
dgst digest.Digest
expectedError string
}{
{
name: "digests do not match",
s: "quay.io/jetstack/cert-manager-webhook@sha256:13fd9eaadb4e491ef0e1d82de60cb199f5ad2ea5a3f8e0c19fdf31d91175b9cb",
dgst: digest.Digest("sha256:ec4306b243d98cce7c3b1f994f2dae660059ef521b2b24588cfdc950bd816d4c"),
expectedError: "invalid digest set does not match parsed digest: quay.io/jetstack/cert-manager-webhook@sha256:13fd9eaadb4e491ef0e1d82de60cb199f5ad2ea5a3f8e0c19fdf31d91175b9cb sha256:13fd9eaadb4e491ef0e1d82de60cb199f5ad2ea5a3f8e0c19fdf31d91175b9cb",
},
{
name: "no tag or digest",
s: "ghcr.io/spegel-org/spegel",
dgst: "",
expectedError: "image needs to contain a digest",
},
{
name: "reference contains protocol",
s: "https://example.com/test:latest",
dgst: "",
expectedError: "invalid reference",
},
{
name: "unparsable url",
s: "example%#$.com/foo",
dgst: "",
expectedError: "parse \"dummy://example%\": invalid URL escape \"%\"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, err := ParseImageRequireDigest(tt.s, tt.dgst)
require.EqualError(t, err, tt.expectedError)
})
}
}
func TestParseImageNoTagOrDigest(t *testing.T) {
_, err := Parse("ghcr.io/spegel-org/spegel", digest.Digest(""))
require.EqualError(t, err, "image needs to contain a digest")
func TestNewImageErrors(t *testing.T) {
t.Parallel()
// TODO (phillebaba): Add test case for no digest or tag. One needs to be set.
tests := []struct {
name string
registry string
repository string
tag string
dgst digest.Digest
expectedError string
}{
{
name: "missing registry",
registry: "",
repository: "foo/bar",
tag: "latest",
dgst: digest.Digest("sha256:ec4306b243d98cce7c3b1f994f2dae660059ef521b2b24588cfdc950bd816d4c"),
expectedError: "image needs to contain a registry",
},
{
name: "missing repository",
registry: "example.com",
repository: "",
tag: "latest",
dgst: digest.Digest("sha256:ec4306b243d98cce7c3b1f994f2dae660059ef521b2b24588cfdc950bd816d4c"),
expectedError: "image needs to contain a repository",
},
{
name: "invalid digest",
registry: "example.com",
repository: "foo/bar",
tag: "latest",
dgst: digest.Digest("test"),
expectedError: "invalid checksum digest format",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, err := NewImage(tt.registry, tt.repository, tt.tag, tt.dgst)
require.EqualError(t, err, tt.expectedError)
})
}
}

133
pkg/oci/memory.go Normal file
View File

@ -0,0 +1,133 @@
package oci
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"sync"
"github.com/opencontainers/go-digest"
)
var _ Store = &Memory{}
type Memory struct {
blobs map[digest.Digest][]byte
tags map[string]digest.Digest
images []Image
mx sync.RWMutex
}
func NewMemory() *Memory {
return &Memory{
images: []Image{},
tags: map[string]digest.Digest{},
blobs: map[digest.Digest][]byte{},
}
}
func (m *Memory) Name() string {
return "memory"
}
func (m *Memory) Verify(ctx context.Context) error {
return nil
}
func (m *Memory) Subscribe(ctx context.Context) (<-chan OCIEvent, error) {
return nil, nil
}
func (m *Memory) ListImages(ctx context.Context) ([]Image, error) {
m.mx.RLock()
defer m.mx.RUnlock()
return m.images, nil
}
func (m *Memory) Resolve(ctx context.Context, ref string) (digest.Digest, error) {
m.mx.RLock()
defer m.mx.RUnlock()
dgst, ok := m.tags[ref]
if !ok {
return "", fmt.Errorf("could not resolve tag %s to a digest", ref)
}
return dgst, nil
}
func (m *Memory) ListContents(ctx context.Context) ([]Content, error) {
m.mx.RLock()
defer m.mx.RUnlock()
contents := []Content{}
for k := range m.blobs {
contents = append(contents, Content{Digest: k})
}
return contents, nil
}
func (m *Memory) Size(ctx context.Context, dgst digest.Digest) (int64, error) {
m.mx.RLock()
defer m.mx.RUnlock()
b, ok := m.blobs[dgst]
if !ok {
return 0, errors.Join(ErrNotFound, fmt.Errorf("size information for digest %s not found", dgst))
}
return int64(len(b)), nil
}
func (m *Memory) GetManifest(ctx context.Context, dgst digest.Digest) ([]byte, string, error) {
m.mx.RLock()
defer m.mx.RUnlock()
b, ok := m.blobs[dgst]
if !ok {
return nil, "", errors.Join(ErrNotFound, fmt.Errorf("manifest with digest %s not found", dgst))
}
mt, err := DetermineMediaType(b)
if err != nil {
return nil, "", err
}
return b, mt, nil
}
func (m *Memory) GetBlob(ctx context.Context, dgst digest.Digest) (io.ReadSeekCloser, error) {
m.mx.RLock()
defer m.mx.RUnlock()
b, ok := m.blobs[dgst]
if !ok {
return nil, errors.Join(ErrNotFound, fmt.Errorf("blob with digest %s not found", dgst))
}
rc := io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b)))
return struct {
io.ReadSeeker
io.Closer
}{
ReadSeeker: rc,
Closer: io.NopCloser(nil),
}, nil
}
func (m *Memory) AddImage(img Image) {
m.mx.Lock()
defer m.mx.Unlock()
m.images = append(m.images, img)
tagName, ok := img.TagName()
if !ok {
return
}
m.tags[tagName] = img.Digest
}
func (m *Memory) AddBlob(b []byte, dgst digest.Digest) {
m.mx.Lock()
defer m.mx.Unlock()
m.blobs[dgst] = b
}

View File

@ -1,60 +0,0 @@
package oci
import (
"context"
"io"
"github.com/opencontainers/go-digest"
)
var _ Client = &MockClient{}
type MockClient struct {
images []Image
}
func NewMockClient(images []Image) *MockClient {
return &MockClient{
images: images,
}
}
func (m *MockClient) Name() string {
return "mock"
}
func (m *MockClient) Verify(ctx context.Context) error {
return nil
}
func (m *MockClient) Subscribe(ctx context.Context) (<-chan ImageEvent, <-chan error) {
return nil, nil
}
func (m *MockClient) ListImages(ctx context.Context) ([]Image, error) {
return m.images, nil
}
func (m *MockClient) AllIdentifiers(ctx context.Context, img Image) ([]string, error) {
return []string{img.Digest.String()}, nil
}
func (m *MockClient) Resolve(ctx context.Context, ref string) (digest.Digest, error) {
return "", nil
}
func (m *MockClient) Size(ctx context.Context, dgst digest.Digest) (int64, error) {
return 0, nil
}
func (m *MockClient) GetManifest(ctx context.Context, dgst digest.Digest) ([]byte, string, error) {
return nil, "", nil
}
func (m *MockClient) GetBlob(ctx context.Context, dgst digest.Digest) (io.ReadCloser, error) {
return nil, nil
}
func (m *MockClient) CopyLayer(ctx context.Context, dgst digest.Digest, dst io.Writer) error {
return nil
}

View File

@ -2,25 +2,171 @@ package oci
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"github.com/containerd/containerd/v2/core/images"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
type UnknownDocument struct {
MediaType string `json:"mediaType,omitempty"`
var (
ErrNotFound = errors.New("content not found")
)
type EventType string
const (
CreateEvent EventType = "CREATE"
DeleteEvent EventType = "DELETE"
)
type OCIEvent struct {
Type EventType
Key string
}
type Client interface {
Name() string
Verify(ctx context.Context) error
Subscribe(ctx context.Context) (<-chan ImageEvent, <-chan error)
ListImages(ctx context.Context) ([]Image, error)
AllIdentifiers(ctx context.Context, img Image) ([]string, error)
Resolve(ctx context.Context, ref string) (digest.Digest, error)
Size(ctx context.Context, dgst digest.Digest) (int64, error)
GetManifest(ctx context.Context, dgst digest.Digest) ([]byte, string, error)
GetBlob(ctx context.Context, dgst digest.Digest) (io.ReadCloser, error)
// Deprecated: Use GetBlob.
CopyLayer(ctx context.Context, dgst digest.Digest, dst io.Writer) error
type Content struct {
Digest digest.Digest
Registires []string
}
type Store interface {
// Name returns the name of the store implementation.
Name() string
// Verify checks that all expected configuration is set.
Verify(ctx context.Context) error
// Subscribe will notify for any image events ocuring in the store backend.
Subscribe(ctx context.Context) (<-chan OCIEvent, error)
// ListImages returns a list of all local images.
ListImages(ctx context.Context) ([]Image, error)
// Resolve returns the digest for the tagged image name reference.
// The ref is expected to be in the format `registry/name:tag`.
Resolve(ctx context.Context, ref string) (digest.Digest, error)
// ListContents returns a list of all the contents.
ListContents(ctx context.Context) ([]Content, error)
// Size returns the content byte size for the given digest.
// Will return ErrNotFound if the digest cannot be found.
Size(ctx context.Context, dgst digest.Digest) (int64, error)
// GetManifest returns the manifest content for the given digest.
// Will return ErrNotFound if the digest cannot be found.
GetManifest(ctx context.Context, dgst digest.Digest) ([]byte, string, error)
// GetBlob returns a stream of the blob content for the given digest.
// Will return ErrNotFound if the digest cannot be found.
GetBlob(ctx context.Context, dgst digest.Digest) (io.ReadSeekCloser, error)
}
type UnknownDocument struct {
MediaType string `json:"mediaType"`
specs.Versioned
}
func DetermineMediaType(b []byte) (string, error) {
var ud UnknownDocument
if err := json.Unmarshal(b, &ud); err != nil {
return "", err
}
if ud.SchemaVersion == 2 && ud.MediaType != "" {
return ud.MediaType, nil
}
data := map[string]json.RawMessage{}
if err := json.Unmarshal(b, &data); err != nil {
return "", err
}
_, architectureOk := data["architecture"]
_, osOk := data["os"]
_, rootfsOk := data["rootfs"]
if architectureOk && osOk && rootfsOk {
return ocispec.MediaTypeImageConfig, nil
}
_, manifestsOk := data["manifests"]
if ud.SchemaVersion == 2 && manifestsOk {
return ocispec.MediaTypeImageIndex, nil
}
_, configOk := data["config"]
if ud.SchemaVersion == 2 && configOk {
return ocispec.MediaTypeImageManifest, nil
}
return "", errors.New("not able to determine media type")
}
func WalkImage(ctx context.Context, store Store, img Image) ([]digest.Digest, error) {
dgsts := []digest.Digest{}
err := walk(ctx, []digest.Digest{img.Digest}, func(dgst digest.Digest) ([]digest.Digest, error) {
b, mt, err := store.GetManifest(ctx, dgst)
if err != nil {
return nil, err
}
dgsts = append(dgsts, dgst)
switch mt {
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
var idx ocispec.Index
if err := json.Unmarshal(b, &idx); err != nil {
return nil, err
}
manifestDgsts := []digest.Digest{}
for _, m := range idx.Manifests {
_, err := store.Size(ctx, m.Digest)
if errors.Is(err, ErrNotFound) {
continue
}
if err != nil {
return nil, err
}
manifestDgsts = append(manifestDgsts, m.Digest)
}
if len(manifestDgsts) == 0 {
return nil, fmt.Errorf("could not find any platforms with local content in manifest %s", dgst)
}
return manifestDgsts, nil
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
var manifest ocispec.Manifest
err := json.Unmarshal(b, &manifest)
if err != nil {
return nil, err
}
dgsts = append(dgsts, manifest.Config.Digest)
for _, layer := range manifest.Layers {
dgsts = append(dgsts, layer.Digest)
}
return nil, nil
default:
return nil, fmt.Errorf("unexpected media type %s for digest %s", mt, dgst)
}
})
if err != nil {
return nil, fmt.Errorf("failed to walk image manifests: %w", err)
}
if len(dgsts) == 0 {
return nil, errors.New("no image digests found")
}
return dgsts, nil
}
func walk(ctx context.Context, dgsts []digest.Digest, handler func(dgst digest.Digest) ([]digest.Digest, error)) error {
for _, dgst := range dgsts {
children, err := handler(dgst)
if err != nil {
return err
}
if len(children) == 0 {
continue
}
err = walk(ctx, children, handler)
if err != nil {
return err
}
}
return nil
}

View File

@ -1,27 +1,29 @@
package oci
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path"
"path/filepath"
"testing"
"github.com/containerd/containerd"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/content/local"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/metadata"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/metadata"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/containerd/v2/plugins/content/local"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/require"
bolt "go.etcd.io/bbolt"
)
func TestOCIClient(t *testing.T) {
func TestStore(t *testing.T) {
t.Parallel()
b, err := os.ReadFile("./testdata/images.json")
require.NoError(t, err)
imgs := []map[string]string{}
@ -44,11 +46,11 @@ func TestOCIClient(t *testing.T) {
contentPath := t.TempDir()
contentStore, err := local.NewStore(contentPath)
require.NoError(t, err)
boltDB, err := bolt.Open(path.Join(t.TempDir(), "bolt.db"), 0644, nil)
boltDB, err := bolt.Open(path.Join(t.TempDir(), "bolt.db"), 0o644, nil)
require.NoError(t, err)
db := metadata.NewDB(boltDB, contentStore, nil)
imageStore := metadata.NewImageStore(db)
ctx := namespaces.WithNamespace(context.TODO(), "k8s.io")
ctx := namespaces.WithNamespace(t.Context(), "k8s.io")
for _, img := range imgs {
dgst, err := digest.Parse(img["digest"])
require.NoError(t, err)
@ -72,7 +74,7 @@ func TestOCIClient(t *testing.T) {
require.NoError(t, err)
writer.Close()
}
containerdClient, err := containerd.New("", containerd.WithServices(containerd.WithImageStore(imageStore), containerd.WithContentStore(contentStore)))
containerdClient, err := client.New("", client.WithServices(client.WithImageStore(imageStore), client.WithContentStore(contentStore)))
require.NoError(t, err)
remoteContainerd := &Containerd{
client: containerdClient,
@ -82,25 +84,55 @@ func TestOCIClient(t *testing.T) {
client: containerdClient,
}
for _, ociClient := range []Client{remoteContainerd, localContainerd} {
t.Run(ociClient.Name(), func(t *testing.T) {
imgs, err := ociClient.ListImages(ctx)
memoryClient := NewMemory()
for _, img := range imgs {
dgst, err := digest.Parse(img["digest"])
require.NoError(t, err)
img, err := ParseImageRequireDigest(img["name"], dgst)
require.NoError(t, err)
memoryClient.AddImage(img)
}
for k, v := range blobs {
memoryClient.AddBlob(v, k)
}
for _, ociStore := range []Store{remoteContainerd, localContainerd, memoryClient} {
t.Run(ociStore.Name(), func(t *testing.T) {
t.Parallel()
b, mt, err := ociStore.GetManifest(ctx, digest.FromString("foo"))
require.Empty(t, b)
require.Empty(t, mt)
require.ErrorIs(t, err, ErrNotFound)
rc, err := ociStore.GetBlob(ctx, digest.FromString("foo"))
require.Empty(t, rc)
require.ErrorIs(t, err, ErrNotFound)
size, err := ociStore.Size(ctx, digest.FromString("foo"))
require.Empty(t, size)
require.ErrorIs(t, err, ErrNotFound)
imgs, err := ociStore.ListImages(ctx)
require.NoError(t, err)
require.Len(t, imgs, 5)
for _, img := range imgs {
_, err := ociClient.Resolve(ctx, img.Name)
tagName, ok := img.TagName()
require.True(t, ok)
_, err := ociStore.Resolve(ctx, tagName)
require.NoError(t, err)
}
noPlatformName := "example.com/org/no-platform:test"
dgst, err := ociClient.Resolve(ctx, noPlatformName)
require.NoError(t, err)
img := Image{
Name: noPlatformName,
Digest: dgst,
noPlatformImg := Image{
Registry: "example.com",
Repository: "org/no-platform",
Tag: "test",
}
_, err = ociClient.AllIdentifiers(ctx, img)
require.EqualError(t, err, "failed to walk image manifests: could not find any platforms with local content in manifest list: sha256:addc990c58744bdf96364fe89bd4aab38b1e824d51c688edb36c75247cd45fa9")
tagName, ok := noPlatformImg.TagName()
require.True(t, ok)
dgst, err := ociStore.Resolve(ctx, tagName)
require.NoError(t, err)
noPlatformImg.Digest = dgst
_, err = WalkImage(ctx, ociStore, noPlatformImg)
require.EqualError(t, err, "failed to walk image manifests: could not find any platforms with local content in manifest sha256:addc990c58744bdf96364fe89bd4aab38b1e824d51c688edb36c75247cd45fa9")
contentTests := []struct {
mediaType string
@ -130,16 +162,18 @@ func TestOCIClient(t *testing.T) {
}
for _, tt := range contentTests {
t.Run(tt.mediaType, func(t *testing.T) {
size, err := ociClient.Size(ctx, tt.dgst)
t.Parallel()
size, err := ociStore.Size(ctx, tt.dgst)
require.NoError(t, err)
require.Equal(t, tt.size, size)
if tt.mediaType != ocispec.MediaTypeImageLayer {
b, mediaType, err := ociClient.GetManifest(ctx, tt.dgst)
b, mediaType, err := ociStore.GetManifest(ctx, tt.dgst)
require.NoError(t, err)
require.Equal(t, tt.mediaType, mediaType)
require.Equal(t, blobs[tt.dgst], b)
} else {
rc, err := ociClient.GetBlob(ctx, tt.dgst)
rc, err := ociStore.GetBlob(ctx, tt.dgst)
require.NoError(t, err)
defer rc.Close()
b, err := io.ReadAll(rc)
@ -150,14 +184,14 @@ func TestOCIClient(t *testing.T) {
}
identifiersTests := []struct {
imageName string
imageDigest string
expectedKeys []string
imageName string
imageDigest string
expectedDgsts []digest.Digest
}{
{
imageName: "ghcr.io/spegel-org/spegel:v0.0.8-with-media-type",
imageDigest: "sha256:9506c8e7a2d0a098d43cadfd7ecdc3c91697e8188d3a1245943b669f717747b4",
expectedKeys: []string{
expectedDgsts: []digest.Digest{
"sha256:9506c8e7a2d0a098d43cadfd7ecdc3c91697e8188d3a1245943b669f717747b4",
"sha256:44cb2cf712c060f69df7310e99339c1eb51a085446f1bb6d44469acff35b4355",
"sha256:d715ba0d85ee7d37da627d0679652680ed2cb23dde6120f25143a0b8079ee47e",
@ -203,7 +237,7 @@ func TestOCIClient(t *testing.T) {
{
imageName: "ghcr.io/spegel-org/spegel:v0.0.8-without-media-type",
imageDigest: "sha256:d8df04365d06181f037251de953aca85cc16457581a8fc168f4957c978e1008b",
expectedKeys: []string{
expectedDgsts: []digest.Digest{
"sha256:d8df04365d06181f037251de953aca85cc16457581a8fc168f4957c978e1008b",
"sha256:44cb2cf712c060f69df7310e99339c1eb51a085446f1bb6d44469acff35b4355",
"sha256:d715ba0d85ee7d37da627d0679652680ed2cb23dde6120f25143a0b8079ee47e",
@ -249,13 +283,66 @@ func TestOCIClient(t *testing.T) {
}
for _, tt := range identifiersTests {
t.Run(tt.imageName, func(t *testing.T) {
img, err := Parse(tt.imageName, digest.Digest(tt.imageDigest))
t.Parallel()
img, err := ParseImageRequireDigest(tt.imageName, digest.Digest(tt.imageDigest))
require.NoError(t, err)
keys, err := ociClient.AllIdentifiers(ctx, img)
dgsts, err := WalkImage(ctx, ociStore, img)
require.NoError(t, err)
require.Equal(t, tt.expectedKeys, keys)
require.Equal(t, tt.expectedDgsts, dgsts)
})
}
})
}
}
func TestDetermineMediaType(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dgst digest.Digest
expectedMediaType string
}{
{
name: "image config",
dgst: digest.Digest("sha256:68b8a989a3e08ddbdb3a0077d35c0d0e59c9ecf23d0634584def8bdbb7d6824f"),
expectedMediaType: ocispec.MediaTypeImageConfig,
},
{
name: "image index",
dgst: digest.Digest("sha256:9430beb291fa7b96997711fc486bc46133c719631aefdbeebe58dd3489217bfe"),
expectedMediaType: ocispec.MediaTypeImageIndex,
},
{
name: "image index without media type",
dgst: digest.Digest("sha256:d8df04365d06181f037251de953aca85cc16457581a8fc168f4957c978e1008b"),
expectedMediaType: ocispec.MediaTypeImageIndex,
},
{
name: "image manifest",
dgst: digest.Digest("sha256:dce623533c59af554b85f859e91fc1cbb7f574e873c82f36b9ea05a09feb0b53"),
expectedMediaType: ocispec.MediaTypeImageManifest,
},
{
name: "image manifest without media type",
dgst: digest.Digest("sha256:b6d6089ca6c395fd563c2084f5dd7bc56a2f5e6a81413558c5be0083287a77e9"),
expectedMediaType: ocispec.MediaTypeImageManifest,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
b, err := os.ReadFile(filepath.Join("testdata", "blobs", tt.dgst.Algorithm().String(), tt.dgst.Encoded()))
require.NoError(t, err)
mt, err := DetermineMediaType(b)
require.NoError(t, err)
require.Equal(t, tt.expectedMediaType, mt)
})
}
mt, err := DetermineMediaType([]byte("{}"))
require.EqualError(t, err, "not able to determine media type")
require.Empty(t, mt)
}

View File

@ -0,0 +1,15 @@
{
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:68b8a989a3e08ddbdb3a0077d35c0d0e59c9ecf23d0634584def8bdbb7d6824f",
"size": 529
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:3caa2469de2a23cbcc209dd0b9d01cd78ff9a0f88741655991d36baede5b0996",
"size": 118
}
]
}

View File

@ -1,48 +0,0 @@
package registry
import (
"fmt"
"regexp"
"github.com/opencontainers/go-digest"
)
type referenceType string
const (
referenceTypeManifest = "Manifest"
referenceTypeBlob = "Blob"
)
// Package is used to parse components from requests which comform with the OCI distribution spec.
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md
// /v2/<name>/manifests/<reference>
// /v2/<name>/blobs/<reference>
var (
nameRegex = regexp.MustCompile(`([a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*)`)
tagRegex = regexp.MustCompile(`([a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})`)
manifestRegexTag = regexp.MustCompile(`/v2/` + nameRegex.String() + `/manifests/` + tagRegex.String() + `$`)
manifestRegexDigest = regexp.MustCompile(`/v2/` + nameRegex.String() + `/manifests/(.*)`)
blobsRegexDigest = regexp.MustCompile(`/v2/` + nameRegex.String() + `/blobs/(.*)`)
)
func parsePathComponents(registry, path string) (string, digest.Digest, referenceType, error) {
comps := manifestRegexTag.FindStringSubmatch(path)
if len(comps) == 6 {
if registry == "" {
return "", "", "", fmt.Errorf("registry parameter needs to be set for tag references")
}
ref := fmt.Sprintf("%s/%s:%s", registry, comps[1], comps[5])
return ref, "", referenceTypeManifest, nil
}
comps = manifestRegexDigest.FindStringSubmatch(path)
if len(comps) == 6 {
return "", digest.Digest(comps[5]), referenceTypeManifest, nil
}
comps = blobsRegexDigest.FindStringSubmatch(path)
if len(comps) == 6 {
return "", digest.Digest(comps[5]), referenceTypeBlob, nil
}
return "", "", "", fmt.Errorf("distribution path could not be parsed")
}

View File

@ -1,55 +0,0 @@
package registry
import (
"testing"
"github.com/opencontainers/go-digest"
"github.com/stretchr/testify/require"
)
func TestParsePathComponents(t *testing.T) {
tests := []struct {
name string
registry string
path string
expectedRef string
expectedDgst digest.Digest
expectedRefType referenceType
}{
{
name: "valid manifest tag",
registry: "example.com",
path: "/v2/foo/bar/manifests/hello-world",
expectedRef: "example.com/foo/bar:hello-world",
expectedDgst: "",
expectedRefType: referenceTypeManifest,
},
{
name: "valid blob digest",
registry: "docker.io",
path: "/v2/library/nginx/blobs/sha256:295c7be079025306c4f1d65997fcf7adb411c88f139ad1d34b537164aa060369",
expectedRef: "",
expectedDgst: digest.Digest("sha256:295c7be079025306c4f1d65997fcf7adb411c88f139ad1d34b537164aa060369"),
expectedRefType: referenceTypeBlob,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ref, dgst, refType, err := parsePathComponents(tt.registry, tt.path)
require.NoError(t, err)
require.Equal(t, tt.expectedRef, ref)
require.Equal(t, tt.expectedDgst, dgst)
require.Equal(t, tt.expectedRefType, refType)
})
}
}
func TestParsePathComponentsInvalidPath(t *testing.T) {
_, _, _, err := parsePathComponents("example.com", "/v2/spegel-org/spegel/v0.0.1")
require.EqualError(t, err, "distribution path could not be parsed")
}
func TestParsePathComponentsMissingRegistry(t *testing.T) {
_, _, _, err := parsePathComponents("", "/v2/spegel-org/spegel/manifests/v0.0.1")
require.EqualError(t, err, "registry parameter needs to be set for tag references")
}

View File

@ -2,157 +2,169 @@ package registry
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/netip"
"net/url"
"path"
"strconv"
"strings"
"sync"
"time"
"github.com/go-logr/logr"
"github.com/opencontainers/go-digest"
"github.com/spegel-org/spegel/internal/mux"
"github.com/spegel-org/spegel/pkg/httpx"
"github.com/spegel-org/spegel/pkg/metrics"
"github.com/spegel-org/spegel/pkg/oci"
"github.com/spegel-org/spegel/pkg/routing"
"github.com/spegel-org/spegel/pkg/throttle"
)
const (
MirroredHeaderKey = "X-Spegel-Mirrored"
HeaderSpegelMirrored = "X-Spegel-Mirrored"
)
type RegistryConfig struct {
Client *http.Client
Log logr.Logger
Username string
Password string
ResolveRetries int
ResolveLatestTag bool
ResolveTimeout time.Duration
}
func (cfg *RegistryConfig) Apply(opts ...RegistryOption) error {
for _, opt := range opts {
if opt == nil {
continue
}
if err := opt(cfg); err != nil {
return err
}
}
return nil
}
type RegistryOption func(cfg *RegistryConfig) error
func WithResolveRetries(resolveRetries int) RegistryOption {
return func(cfg *RegistryConfig) error {
cfg.ResolveRetries = resolveRetries
return nil
}
}
func WithResolveLatestTag(resolveLatestTag bool) RegistryOption {
return func(cfg *RegistryConfig) error {
cfg.ResolveLatestTag = resolveLatestTag
return nil
}
}
func WithResolveTimeout(resolveTimeout time.Duration) RegistryOption {
return func(cfg *RegistryConfig) error {
cfg.ResolveTimeout = resolveTimeout
return nil
}
}
func WithTransport(transport http.RoundTripper) RegistryOption {
return func(cfg *RegistryConfig) error {
if cfg.Client == nil {
cfg.Client = &http.Client{}
}
cfg.Client.Transport = transport
return nil
}
}
func WithLogger(log logr.Logger) RegistryOption {
return func(cfg *RegistryConfig) error {
cfg.Log = log
return nil
}
}
func WithBasicAuth(username, password string) RegistryOption {
return func(cfg *RegistryConfig) error {
cfg.Username = username
cfg.Password = password
return nil
}
}
type Registry struct {
client *http.Client
bufferPool *sync.Pool
log logr.Logger
throttler *throttle.Throttler
ociClient oci.Client
ociStore oci.Store
router routing.Router
transport http.RoundTripper
localAddr string
username string
password string
resolveRetries int
resolveTimeout time.Duration
resolveLatestTag bool
}
type Option func(*Registry)
func WithResolveRetries(resolveRetries int) Option {
return func(r *Registry) {
r.resolveRetries = resolveRetries
func NewRegistry(ociStore oci.Store, router routing.Router, opts ...RegistryOption) (*Registry, error) {
transport, ok := http.DefaultTransport.(*http.Transport)
if !ok {
return nil, errors.New("default transporn is not of type http.Transport")
}
}
func WithResolveLatestTag(resolveLatestTag bool) Option {
return func(r *Registry) {
r.resolveLatestTag = resolveLatestTag
cfg := RegistryConfig{
Client: &http.Client{
Transport: transport.Clone(),
},
Log: logr.Discard(),
ResolveRetries: 3,
ResolveLatestTag: true,
ResolveTimeout: 20 * time.Millisecond,
}
}
func WithResolveTimeout(resolveTimeout time.Duration) Option {
return func(r *Registry) {
r.resolveTimeout = resolveTimeout
err := cfg.Apply(opts...)
if err != nil {
return nil, err
}
}
func WithTransport(transport http.RoundTripper) Option {
return func(r *Registry) {
r.transport = transport
bufferPool := &sync.Pool{
New: func() any {
buf := make([]byte, 32*1024)
return &buf
},
}
}
func WithLocalAddress(localAddr string) Option {
return func(r *Registry) {
r.localAddr = localAddr
}
}
func WithBlobSpeed(blobSpeed throttle.Byterate) Option {
return func(r *Registry) {
r.throttler = throttle.NewThrottler(blobSpeed)
}
}
func WithLogger(log logr.Logger) Option {
return func(r *Registry) {
r.log = log
}
}
func NewRegistry(ociClient oci.Client, router routing.Router, opts ...Option) *Registry {
r := &Registry{
ociClient: ociClient,
ociStore: ociStore,
router: router,
resolveRetries: 3,
resolveTimeout: 1 * time.Second,
resolveLatestTag: true,
client: cfg.Client,
log: cfg.Log,
resolveRetries: cfg.ResolveRetries,
resolveLatestTag: cfg.ResolveLatestTag,
resolveTimeout: cfg.ResolveTimeout,
username: cfg.Username,
password: cfg.Password,
bufferPool: bufferPool,
}
for _, opt := range opts {
opt(r)
}
return r
return r, nil
}
func (r *Registry) Server(addr string) *http.Server {
func (r *Registry) Server(addr string) (*http.Server, error) {
m := httpx.NewServeMux(r.log)
m.Handle("GET /healthz", r.readyHandler)
m.Handle("GET /v2/", r.registryHandler)
m.Handle("HEAD /v2/", r.registryHandler)
srv := &http.Server{
Addr: addr,
Handler: mux.NewServeMux(r.handle),
Handler: m,
}
return srv
return srv, nil
}
func (r *Registry) handle(rw mux.ResponseWriter, req *http.Request) {
start := time.Now()
handler := req.URL.Path
if strings.HasPrefix(handler, "/v2") {
handler = "/v2/*"
}
defer func() {
latency := time.Since(start)
statusCode := strconv.FormatInt(int64(rw.Status()), 10)
metrics.HttpRequestsInflight.WithLabelValues(handler).Add(-1)
metrics.HttpRequestDurHistogram.WithLabelValues(handler, req.Method, statusCode).Observe(latency.Seconds())
metrics.HttpResponseSizeHistogram.WithLabelValues(handler, req.Method, statusCode).Observe(float64(rw.Size()))
// Ignore logging requests to healthz to reduce log noise
if req.URL.Path == "/healthz" {
return
}
// Logging
ip := getClientIP(req)
path := req.URL.Path
kvs := []interface{}{"path", path, "status", rw.Status(), "method", req.Method, "latency", latency, "ip", ip}
if rw.Status() >= 200 && rw.Status() < 300 {
r.log.Info("", kvs...)
return
}
r.log.Error(rw.Error(), "", kvs...)
}()
metrics.HttpRequestsInflight.WithLabelValues(handler).Add(1)
if req.URL.Path == "/healthz" && req.Method == http.MethodGet {
r.readyHandler(rw, req)
return
}
if strings.HasPrefix(req.URL.Path, "/v2") && (req.Method == http.MethodGet || req.Method == http.MethodHead) {
r.registryHandler(rw, req)
return
}
rw.WriteHeader(http.StatusNotFound)
}
func (r *Registry) readyHandler(rw mux.ResponseWriter, req *http.Request) {
ok, err := r.router.Ready()
func (r *Registry) readyHandler(rw httpx.ResponseWriter, req *http.Request) {
rw.SetHandler("ready")
ok, err := r.router.Ready(req.Context())
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
rw.WriteError(http.StatusInternalServerError, fmt.Errorf("could not determine router readiness: %w", err))
return
}
if !ok {
@ -161,199 +173,213 @@ func (r *Registry) readyHandler(rw mux.ResponseWriter, req *http.Request) {
}
}
func (r *Registry) registryHandler(rw mux.ResponseWriter, req *http.Request) {
func (r *Registry) registryHandler(rw httpx.ResponseWriter, req *http.Request) {
rw.SetHandler("registry")
// Check basic authentication
if r.username != "" || r.password != "" {
username, password, _ := req.BasicAuth()
if r.username != username || r.password != password {
rw.WriteError(http.StatusUnauthorized, errors.New("invalid basic authentication"))
return
}
}
// Quickly return 200 for /v2 to indicate that registry supports v2.
if path.Clean(req.URL.Path) == "/v2" {
rw.SetHandler("v2")
rw.WriteHeader(http.StatusOK)
return
}
// Parse out path components from request.
registryName := req.URL.Query().Get("ns")
ref, dgst, refType, err := parsePathComponents(registryName, req.URL.Path)
dist, err := oci.ParseDistributionPath(req.URL)
if err != nil {
rw.WriteError(http.StatusNotFound, err)
rw.WriteError(http.StatusNotFound, fmt.Errorf("could not parse path according to OCI distribution spec: %w", err))
return
}
// Check if latest tag should be resolved
if !r.resolveLatestTag && ref != "" {
_, tag, _ := strings.Cut(ref, ":")
if tag == "latest" {
rw.WriteHeader(http.StatusNotFound)
// Request with mirror header are proxied.
if req.Header.Get(HeaderSpegelMirrored) != "true" {
// Set mirrored header in request to stop infinite loops
req.Header.Set(HeaderSpegelMirrored, "true")
// If content is present locally we should skip the mirroring and just serve it.
var ociErr error
if dist.Digest == "" {
_, ociErr = r.ociStore.Resolve(req.Context(), dist.Reference())
} else {
_, ociErr = r.ociStore.Size(req.Context(), dist.Digest)
}
if ociErr != nil {
rw.SetHandler("mirror")
r.handleMirror(rw, req, dist)
return
}
}
// Request with mirror header are proxied.
if req.Header.Get(MirroredHeaderKey) != "true" {
// Set mirrored header in request to stop infinite loops
req.Header.Set(MirroredHeaderKey, "true")
key := dgst.String()
if key == "" {
key = ref
}
r.handleMirror(rw, req, key)
sourceType := "internal"
if r.isExternalRequest(req) {
sourceType = "external"
}
// Serve registry endpoints.
switch dist.Kind {
case oci.DistributionKindManifest:
rw.SetHandler("manifest")
r.handleManifest(rw, req, dist)
return
case oci.DistributionKindBlob:
rw.SetHandler("blob")
r.handleBlob(rw, req, dist)
return
default:
rw.WriteError(http.StatusNotFound, fmt.Errorf("unknown distribution path kind %s", dist.Kind))
return
}
}
func (r *Registry) handleMirror(rw httpx.ResponseWriter, req *http.Request, dist oci.DistributionPath) {
log := r.log.WithValues("ref", dist.Reference(), "path", req.URL.Path)
defer func() {
cacheType := "hit"
if rw.Status() != http.StatusOK {
cacheType = "miss"
}
metrics.MirrorRequestsTotal.WithLabelValues(registryName, cacheType, sourceType).Inc()
metrics.MirrorRequestsTotal.WithLabelValues(dist.Registry, cacheType).Inc()
}()
if !r.resolveLatestTag && dist.IsLatestTag() {
r.log.V(4).Info("skipping mirror request for image with latest tag", "image", dist.Reference())
rw.WriteHeader(http.StatusNotFound)
return
}
// Serve registry endpoints.
if dgst == "" {
dgst, err = r.ociClient.Resolve(req.Context(), ref)
if err != nil {
rw.WriteError(http.StatusNotFound, err)
return
}
}
switch refType {
case referenceTypeManifest:
r.handleManifest(rw, req, dgst)
case referenceTypeBlob:
r.handleBlob(rw, req, dgst)
default:
// If nothing matches return 404.
rw.WriteHeader(http.StatusNotFound)
}
}
func (r *Registry) handleMirror(rw mux.ResponseWriter, req *http.Request, key string) {
log := r.log.WithValues("key", key, "path", req.URL.Path, "ip", req.RemoteAddr)
// Resolve mirror with the requested key
// Resolve mirror with the requested reference
resolveCtx, cancel := context.WithTimeout(req.Context(), r.resolveTimeout)
defer cancel()
resolveCtx = logr.NewContext(resolveCtx, log)
isExternal := r.isExternalRequest(req)
if isExternal {
log.Info("handling mirror request from external node")
}
peerCh, err := r.router.Resolve(resolveCtx, key, isExternal, r.resolveRetries)
peerCh, err := r.router.Resolve(resolveCtx, dist.Reference(), r.resolveRetries)
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
rw.WriteError(http.StatusInternalServerError, fmt.Errorf("error occurred when attempting to resolve mirrors: %w", err))
return
}
// TODO: Refactor context cancel and mirror channel closing
mirrorAttempts := 0
for {
select {
case <-resolveCtx.Done():
case <-req.Context().Done():
// Request has been closed by server or client. No use continuing.
rw.WriteError(http.StatusNotFound, fmt.Errorf("request closed for key: %s", key))
rw.WriteError(http.StatusNotFound, fmt.Errorf("mirroring for image component %s has been cancelled: %w", dist.Reference(), resolveCtx.Err()))
return
case ipAddr, ok := <-peerCh:
case peer, ok := <-peerCh:
// Channel closed means no more mirrors will be received and max retries has been reached.
if !ok {
rw.WriteError(http.StatusNotFound, fmt.Errorf("mirror resolve retries exhausted for key: %s", key))
err = fmt.Errorf("mirror with image component %s could not be found", dist.Reference())
if mirrorAttempts > 0 {
err = errors.Join(err, fmt.Errorf("requests to %d mirrors failed, all attempts have been exhausted or timeout has been reached", mirrorAttempts))
}
rw.WriteError(http.StatusNotFound, err)
return
}
// Modify response returns and error on non 200 status code and NOP error handler skips response writing.
// If proxy fails no response is written and it is tried again against a different mirror.
// If the response writer has been written to it means that the request was properly proxied.
succeeded := false
scheme := "http"
if req.TLS != nil {
scheme = "https"
mirrorAttempts++
err := forwardRequest(r.client, r.bufferPool, req, rw, peer)
if err != nil {
log.Error(err, "request to mirror failed", "attempt", mirrorAttempts, "path", req.URL.Path, "mirror", peer)
continue
}
u := &url.URL{
Scheme: scheme,
Host: ipAddr.String(),
}
proxy := httputil.NewSingleHostReverseProxy(u)
proxy.Transport = r.transport
proxy.ErrorHandler = func(_ http.ResponseWriter, _ *http.Request, err error) {
log.Error(err, "proxy failed attempting next")
}
proxy.ModifyResponse = func(resp *http.Response) error {
if resp.StatusCode != http.StatusOK {
err := fmt.Errorf("expected mirror to respond with 200 OK but received: %s", resp.Status)
log.Error(err, "mirror failed attempting next")
return err
}
succeeded = true
return nil
}
proxy.ServeHTTP(rw, req)
if !succeeded {
break
}
log.V(5).Info("mirrored request", "url", u.String())
log.V(4).Info("mirrored request", "path", req.URL.Path, "mirror", peer)
return
}
}
}
func (r *Registry) handleManifest(rw mux.ResponseWriter, req *http.Request, dgst digest.Digest) {
b, mediaType, err := r.ociClient.GetManifest(req.Context(), dgst)
func (r *Registry) handleManifest(rw httpx.ResponseWriter, req *http.Request, dist oci.DistributionPath) {
if dist.Digest == "" {
dgst, err := r.ociStore.Resolve(req.Context(), dist.Reference())
if err != nil {
rw.WriteError(http.StatusNotFound, fmt.Errorf("could not get digest for image %s: %w", dist.Reference(), err))
return
}
dist.Digest = dgst
}
b, mediaType, err := r.ociStore.GetManifest(req.Context(), dist.Digest)
if err != nil {
rw.WriteError(http.StatusNotFound, err)
rw.WriteError(http.StatusNotFound, fmt.Errorf("could not get manifest content for digest %s: %w", dist.Digest.String(), err))
return
}
rw.Header().Set("Content-Type", mediaType)
rw.Header().Set("Content-Length", strconv.FormatInt(int64(len(b)), 10))
rw.Header().Set("Docker-Content-Digest", dgst.String())
rw.Header().Set(httpx.HeaderContentType, mediaType)
rw.Header().Set(httpx.HeaderContentLength, strconv.FormatInt(int64(len(b)), 10))
rw.Header().Set(oci.HeaderDockerDigest, dist.Digest.String())
if req.Method == http.MethodHead {
return
}
_, err = rw.Write(b)
if err != nil {
rw.WriteError(http.StatusNotFound, err)
r.log.Error(err, "error occurred when writing manifest")
return
}
}
func (r *Registry) handleBlob(rw mux.ResponseWriter, req *http.Request, dgst digest.Digest) {
size, err := r.ociClient.Size(req.Context(), dgst)
func (r *Registry) handleBlob(rw httpx.ResponseWriter, req *http.Request, dist oci.DistributionPath) {
size, err := r.ociStore.Size(req.Context(), dist.Digest)
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
rw.WriteError(http.StatusInternalServerError, fmt.Errorf("could not determine size of blob with digest %s: %w", dist.Digest.String(), err))
return
}
rw.Header().Set("Content-Length", strconv.FormatInt(size, 10))
rw.Header().Set("Docker-Content-Digest", dgst.String())
rw.Header().Set(httpx.HeaderAcceptRanges, "bytes")
rw.Header().Set(httpx.HeaderContentType, "application/octet-stream")
rw.Header().Set(httpx.HeaderContentLength, strconv.FormatInt(size, 10))
rw.Header().Set(oci.HeaderDockerDigest, dist.Digest.String())
if req.Method == http.MethodHead {
return
}
var w io.Writer = rw
if r.throttler != nil {
w = r.throttler.Writer(rw)
}
rc, err := r.ociClient.GetBlob(req.Context(), dgst)
rc, err := r.ociStore.GetBlob(req.Context(), dist.Digest)
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
rw.WriteError(http.StatusInternalServerError, fmt.Errorf("could not get reader for blob with digest %s: %w", dist.Digest.String(), err))
return
}
defer rc.Close()
_, err = io.Copy(w, rc)
if err != nil {
rw.WriteError(http.StatusInternalServerError, err)
return
}
http.ServeContent(rw, req, "", time.Time{}, rc)
}
func (r *Registry) isExternalRequest(req *http.Request) bool {
return req.Host != r.localAddr
}
func getClientIP(req *http.Request) string {
forwardedFor := req.Header.Get("X-Forwarded-For")
if forwardedFor != "" {
comps := strings.Split(forwardedFor, ",")
if len(comps) > 1 {
return comps[0]
}
return forwardedFor
func forwardRequest(client *http.Client, bufferPool *sync.Pool, req *http.Request, rw http.ResponseWriter, addrPort netip.AddrPort) error {
// Do request to mirror.
forwardScheme := "http"
if req.TLS != nil {
forwardScheme = "https"
}
h, _, err := net.SplitHostPort(req.RemoteAddr)
u := &url.URL{
Scheme: forwardScheme,
Host: addrPort.String(),
Path: req.URL.Path,
RawQuery: req.URL.RawQuery,
}
forwardReq, err := http.NewRequestWithContext(req.Context(), req.Method, u.String(), nil)
if err != nil {
return ""
return err
}
return h
httpx.CopyHeader(forwardReq.Header, req.Header)
forwardResp, err := client.Do(forwardReq)
if err != nil {
return err
}
defer httpx.DrainAndClose(forwardResp.Body)
err = httpx.CheckResponseStatus(forwardResp, http.StatusOK, http.StatusPartialContent)
if err != nil {
return err
}
// TODO (phillebaba): Is it possible to retry if copy fails half way through?
// Copy forward response to response writer.
httpx.CopyHeader(rw.Header(), forwardResp.Header)
rw.WriteHeader(http.StatusOK)
//nolint: errcheck // Ignore
buf := bufferPool.Get().(*[]byte)
defer bufferPool.Put(buf)
_, err = io.CopyBuffer(rw, forwardResp.Body, *buf)
if err != nil {
return err
}
return nil
}

View File

@ -7,14 +7,141 @@ import (
"net/http/httptest"
"net/netip"
"testing"
"time"
"github.com/go-logr/logr"
"github.com/stretchr/testify/require"
"github.com/spegel-org/spegel/internal/mux"
"github.com/spegel-org/spegel/pkg/httpx"
"github.com/spegel-org/spegel/pkg/oci"
"github.com/spegel-org/spegel/pkg/routing"
)
func TestRegistryOptions(t *testing.T) {
t.Parallel()
transport := &http.Transport{}
log := logr.Discard()
opts := []RegistryOption{
WithResolveRetries(5),
WithResolveLatestTag(true),
WithResolveTimeout(10 * time.Minute),
WithTransport(transport),
WithLogger(log),
WithBasicAuth("foo", "bar"),
}
cfg := RegistryConfig{}
err := cfg.Apply(opts...)
require.NoError(t, err)
require.Equal(t, 5, cfg.ResolveRetries)
require.True(t, cfg.ResolveLatestTag)
require.Equal(t, 10*time.Minute, cfg.ResolveTimeout)
require.Equal(t, transport, cfg.Client.Transport)
require.Equal(t, log, cfg.Log)
require.Equal(t, "foo", cfg.Username)
require.Equal(t, "bar", cfg.Password)
}
func TestReadyHandler(t *testing.T) {
t.Parallel()
router := routing.NewMemoryRouter(map[string][]netip.AddrPort{}, netip.MustParseAddrPort("127.0.0.1:8080"))
reg, err := NewRegistry(nil, router)
require.NoError(t, err)
srv, err := reg.Server("")
require.NoError(t, err)
rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "http://localhost/healthz", nil)
srv.Handler.ServeHTTP(rw, req)
require.Equal(t, http.StatusInternalServerError, rw.Result().StatusCode)
router.Add("foo", netip.MustParseAddrPort("127.0.0.1:9090"))
rw = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "http://localhost/healthz", nil)
srv.Handler.ServeHTTP(rw, req)
require.Equal(t, http.StatusOK, rw.Result().StatusCode)
}
func TestBasicAuth(t *testing.T) {
t.Parallel()
tests := []struct {
name string
username string
password string
reqUsername string
reqPassword string
expected int
}{
{
name: "no registry authentication",
expected: http.StatusOK,
},
{
name: "unnecessary authentication",
reqUsername: "foo",
reqPassword: "bar",
expected: http.StatusOK,
},
{
name: "correct authentication",
username: "foo",
password: "bar",
reqUsername: "foo",
reqPassword: "bar",
expected: http.StatusOK,
},
{
name: "invalid username",
username: "foo",
password: "bar",
reqUsername: "wrong",
reqPassword: "bar",
expected: http.StatusUnauthorized,
},
{
name: "invalid password",
username: "foo",
password: "bar",
reqUsername: "foo",
reqPassword: "wrong",
expected: http.StatusUnauthorized,
},
{
name: "missing authentication",
username: "foo",
password: "bar",
expected: http.StatusUnauthorized,
},
{
name: "missing authentication",
username: "foo",
password: "bar",
expected: http.StatusUnauthorized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
reg, err := NewRegistry(nil, nil, WithBasicAuth(tt.username, tt.password))
require.NoError(t, err)
rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "http://localhost/v2/", nil)
req.SetBasicAuth(tt.reqUsername, tt.reqPassword)
srv, err := reg.Server("")
require.NoError(t, err)
srv.Handler.ServeHTTP(rw, req)
require.Equal(t, tt.expected, rw.Result().StatusCode)
})
}
}
func TestMirrorHandler(t *testing.T) {
t.Parallel()
badSvr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("foo", "bar")
@ -23,7 +150,9 @@ func TestMirrorHandler(t *testing.T) {
w.Write([]byte("hello world"))
}
}))
defer badSvr.Close()
t.Cleanup(func() {
badSvr.Close()
})
badAddrPort := netip.MustParseAddrPort(badSvr.Listener.Addr().String())
goodSvr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("foo", "bar")
@ -32,18 +161,25 @@ func TestMirrorHandler(t *testing.T) {
w.Write([]byte("hello world"))
}
}))
defer goodSvr.Close()
t.Cleanup(func() {
goodSvr.Close()
})
goodAddrPort := netip.MustParseAddrPort(goodSvr.Listener.Addr().String())
unreachableAddrPort := netip.MustParseAddrPort("127.0.0.1:0")
resolver := map[string][]netip.AddrPort{
"no-working-peers": {badAddrPort, unreachableAddrPort, badAddrPort},
"first-peer": {goodAddrPort, badAddrPort, badAddrPort},
"first-peer-error": {unreachableAddrPort, goodAddrPort},
"last-peer-working": {badAddrPort, badAddrPort, goodAddrPort},
// No working peers
"sha256:c3e30fbcf3b231356a1efbd30a8ccec75134a7a8b45217ede97f4ff483540b04": {badAddrPort, unreachableAddrPort, badAddrPort},
// First Peer
"sha256:3b8a55c543ccc7ae01c47b1d35af5826a6439a9b91ab0ca96de9967759279896": {goodAddrPort, badAddrPort, badAddrPort},
// First peer error
"sha256:a0daab85ec30e2809a38c32fa676515aba22f481c56fda28637ae964ff398e3d": {unreachableAddrPort, goodAddrPort},
// Last peer working
"sha256:11242d2a347bf8ab30b9f92d5ca219bbbedf95df5a8b74631194561497c1fae8": {badAddrPort, badAddrPort, goodAddrPort},
}
router := routing.NewMockRouter(resolver, netip.AddrPort{})
reg := NewRegistry(nil, router)
router := routing.NewMemoryRouter(resolver, netip.AddrPort{})
reg, err := NewRegistry(oci.NewMemory(), router)
require.NoError(t, err)
tests := []struct {
expectedHeaders map[string][]string
@ -61,28 +197,28 @@ func TestMirrorHandler(t *testing.T) {
},
{
name: "request should not timeout and give 404 if all peers fail",
key: "no-working-peers",
key: "sha256:c3e30fbcf3b231356a1efbd30a8ccec75134a7a8b45217ede97f4ff483540b04",
expectedStatus: http.StatusNotFound,
expectedBody: "",
expectedHeaders: nil,
},
{
name: "request should work when first peer responds",
key: "first-peer",
key: "sha256:3b8a55c543ccc7ae01c47b1d35af5826a6439a9b91ab0ca96de9967759279896",
expectedStatus: http.StatusOK,
expectedBody: "hello world",
expectedHeaders: map[string][]string{"foo": {"bar"}},
},
{
name: "second peer should respond when first gives error",
key: "first-peer-error",
key: "sha256:a0daab85ec30e2809a38c32fa676515aba22f481c56fda28637ae964ff398e3d",
expectedStatus: http.StatusOK,
expectedBody: "hello world",
expectedHeaders: map[string][]string{"foo": {"bar"}},
},
{
name: "last peer should respond when two first fail",
key: "last-peer-working",
key: "sha256:11242d2a347bf8ab30b9f92d5ca219bbbedf95df5a8b74631194561497c1fae8",
expectedStatus: http.StatusOK,
expectedBody: "hello world",
expectedHeaders: map[string][]string{"foo": {"bar"}},
@ -91,14 +227,17 @@ func TestMirrorHandler(t *testing.T) {
for _, tt := range tests {
for _, method := range []string{http.MethodGet, http.MethodHead} {
t.Run(fmt.Sprintf("%s-%s", method, tt.name), func(t *testing.T) {
t.Parallel()
target := fmt.Sprintf("http://example.com/v2/foo/bar/blobs/%s", tt.key)
rw := httptest.NewRecorder()
req := httptest.NewRequest(method, target, nil)
m := mux.NewServeMux(reg.handle)
m.ServeHTTP(rw, req)
srv, err := reg.Server("")
require.NoError(t, err)
srv.Handler.ServeHTTP(rw, req)
resp := rw.Result()
defer resp.Body.Close()
defer httpx.DrainAndClose(resp.Body)
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, tt.expectedStatus, resp.StatusCode)
@ -120,43 +259,3 @@ func TestMirrorHandler(t *testing.T) {
}
}
}
func TestGetClientIP(t *testing.T) {
tests := []struct {
name string
request *http.Request
expected string
}{
{
name: "x forwarded for single",
request: &http.Request{
Header: http.Header{
"X-Forwarded-For": []string{"localhost"},
},
},
expected: "localhost",
},
{
name: "x forwarded for multiple",
request: &http.Request{
Header: http.Header{
"X-Forwarded-For": []string{"localhost,127.0.0.1"},
},
},
expected: "localhost",
},
{
name: "remote address",
request: &http.Request{
RemoteAddr: "127.0.0.1:9090",
},
expected: "127.0.0.1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ip := getClientIP(tt.request)
require.Equal(t, tt.expected, ip)
})
}
}

View File

@ -4,116 +4,138 @@ import (
"context"
"errors"
"io"
"net"
"net/http"
"slices"
"strings"
"sync"
"time"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multiaddr"
"golang.org/x/sync/errgroup"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/client-go/tools/leaderelection/resourcelock"
"github.com/libp2p/go-libp2p/core/peer"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
"github.com/spegel-org/spegel/pkg/httpx"
)
// Bootstrapper resolves peers to bootstrap with for the P2P router.
type Bootstrapper interface {
// Run starts the bootstrap process. Should be blocking even if not needed.
Run(ctx context.Context, id string) error
Get() (*peer.AddrInfo, error)
// Get returns a list of peers that should be used as bootstrap nodes.
// If the peer ID is empty it will be resolved.
// If the address is missing a port the P2P router port will be used.
Get(ctx context.Context) ([]peer.AddrInfo, error)
}
type KubernetesBootstrapper struct {
cs kubernetes.Interface
initCh chan interface{}
leaderElectionNamespace string
leaderElectioName string
id string
mx sync.RWMutex
var _ Bootstrapper = &StaticBootstrapper{}
type StaticBootstrapper struct {
peers []peer.AddrInfo
mx sync.RWMutex
}
func NewKubernetesBootstrapper(cs kubernetes.Interface, namespace, name string) Bootstrapper {
return &KubernetesBootstrapper{
leaderElectionNamespace: namespace,
leaderElectioName: name,
cs: cs,
initCh: make(chan interface{}),
func NewStaticBootstrapperFromStrings(peerStrs []string) (*StaticBootstrapper, error) {
peers := []peer.AddrInfo{}
for _, peerStr := range peerStrs {
peer, err := peer.AddrInfoFromString(peerStr)
if err != nil {
return nil, err
}
peers = append(peers, *peer)
}
return NewStaticBootstrapper(peers), nil
}
func NewStaticBootstrapper(peers []peer.AddrInfo) *StaticBootstrapper {
return &StaticBootstrapper{
peers: peers,
}
}
func (k *KubernetesBootstrapper) Run(ctx context.Context, id string) error {
lockCfg := resourcelock.ResourceLockConfig{
Identity: id,
}
rl, err := resourcelock.New(
resourcelock.LeasesResourceLock,
k.leaderElectionNamespace,
k.leaderElectioName,
k.cs.CoreV1(),
k.cs.CoordinationV1(),
lockCfg,
)
if err != nil {
return err
}
leCfg := leaderelection.LeaderElectionConfig{
Lock: rl,
ReleaseOnCancel: true,
LeaseDuration: 10 * time.Second,
RenewDeadline: 5 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {},
OnStoppedLeading: func() {},
OnNewLeader: func(identity string) {
if identity == resourcelock.UnknownLeader {
return
}
// Close channel if not already closed
select {
case <-k.initCh:
break
default:
close(k.initCh)
}
k.mx.Lock()
defer k.mx.Unlock()
k.id = identity
},
},
}
leaderelection.RunOrDie(ctx, leCfg)
func (b *StaticBootstrapper) Run(ctx context.Context, id string) error {
<-ctx.Done()
return nil
}
func (k *KubernetesBootstrapper) Get() (*peer.AddrInfo, error) {
<-k.initCh
k.mx.RLock()
defer k.mx.RUnlock()
addr, err := multiaddr.NewMultiaddr(k.id)
if err != nil {
return nil, err
}
addrInfo, err := peer.AddrInfoFromP2pAddr(addr)
if err != nil {
return nil, err
}
return addrInfo, err
func (b *StaticBootstrapper) Get(ctx context.Context) ([]peer.AddrInfo, error) {
b.mx.RLock()
defer b.mx.RUnlock()
return b.peers, nil
}
func (b *StaticBootstrapper) SetPeers(peers []peer.AddrInfo) {
b.mx.Lock()
defer b.mx.Unlock()
b.peers = peers
}
var _ Bootstrapper = &DNSBootstrapper{}
type DNSBootstrapper struct {
resolver *net.Resolver
host string
limit int
}
func NewDNSBootstrapper(host string, limit int) *DNSBootstrapper {
return &DNSBootstrapper{
resolver: &net.Resolver{},
host: host,
limit: limit,
}
}
func (b *DNSBootstrapper) Run(ctx context.Context, id string) error {
<-ctx.Done()
return nil
}
func (b *DNSBootstrapper) Get(ctx context.Context) ([]peer.AddrInfo, error) {
ips, err := b.resolver.LookupIPAddr(ctx, b.host)
if err != nil {
return nil, err
}
if len(ips) == 0 {
return nil, err
}
slices.SortFunc(ips, func(a, b net.IPAddr) int {
return strings.Compare(a.String(), b.String())
})
addrInfos := []peer.AddrInfo{}
for _, ip := range ips {
addr, err := manet.FromIPAndZone(ip.IP, ip.Zone)
if err != nil {
return nil, err
}
addrInfos = append(addrInfos, peer.AddrInfo{
ID: "",
Addrs: []ma.Multiaddr{addr},
})
}
limit := min(len(addrInfos), b.limit)
return addrInfos[:limit], nil
}
var _ Bootstrapper = &HTTPBootstrapper{}
type HTTPBootstrapper struct {
addr string
peer string
httpClient *http.Client
addr string
peer string
}
func NewHTTPBootstrapper(addr, peer string) *HTTPBootstrapper {
return &HTTPBootstrapper{
addr: addr,
peer: peer,
httpClient: httpx.BaseClient(),
addr: addr,
peer: peer,
}
}
func (h *HTTPBootstrapper) Run(ctx context.Context, id string) error {
func (bs *HTTPBootstrapper) Run(ctx context.Context, id string) error {
g, ctx := errgroup.WithContext(ctx)
mux := http.NewServeMux()
mux.HandleFunc("/id", func(w http.ResponseWriter, r *http.Request) {
@ -122,7 +144,7 @@ func (h *HTTPBootstrapper) Run(ctx context.Context, id string) error {
w.Write([]byte(id))
})
srv := http.Server{
Addr: h.addr,
Addr: bs.addr,
Handler: mux,
}
g.Go(func() error {
@ -140,17 +162,25 @@ func (h *HTTPBootstrapper) Run(ctx context.Context, id string) error {
return g.Wait()
}
func (h *HTTPBootstrapper) Get() (*peer.AddrInfo, error) {
resp, err := http.DefaultClient.Get(h.peer)
func (bs *HTTPBootstrapper) Get(ctx context.Context) ([]peer.AddrInfo, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, bs.peer, nil)
if err != nil {
return nil, err
}
resp, err := bs.httpClient.Do(req)
if err != nil {
return nil, err
}
defer httpx.DrainAndClose(resp.Body)
err = httpx.CheckResponseStatus(resp, http.StatusOK)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
addr, err := multiaddr.NewMultiaddr(string(b))
addr, err := ma.NewMultiaddr(string(b))
if err != nil {
return nil, err
}
@ -158,5 +188,5 @@ func (h *HTTPBootstrapper) Get() (*peer.AddrInfo, error) {
if err != nil {
return nil, err
}
return addrInfo, err
return []peer.AddrInfo{*addrInfo}, nil
}

View File

@ -6,12 +6,46 @@ import (
"net/http/httptest"
"testing"
"golang.org/x/sync/errgroup"
"github.com/libp2p/go-libp2p/core/peer"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
"github.com/stretchr/testify/require"
)
func TestStaticBootstrap(t *testing.T) {
t.Parallel()
peers := []peer.AddrInfo{
{
ID: "foo",
Addrs: []ma.Multiaddr{ma.StringCast("/ip4/192.168.1.1")},
},
{
ID: "bar",
Addrs: []ma.Multiaddr{manet.IP6Loopback},
},
}
bs := NewStaticBootstrapper(peers)
ctx, cancel := context.WithCancel(t.Context())
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
return bs.Run(gCtx, "")
})
bsPeers, err := bs.Get(t.Context())
require.NoError(t, err)
require.ElementsMatch(t, peers, bsPeers)
cancel()
err = g.Wait()
require.NoError(t, err)
}
func TestHTTPBootstrap(t *testing.T) {
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
t.Parallel()
id := "/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -20,12 +54,23 @@ func TestHTTPBootstrap(t *testing.T) {
}))
defer svr.Close()
bootstrapper := NewHTTPBootstrapper(":", svr.URL)
//nolint:errcheck // ignore
go bootstrapper.Run(ctx, id)
addrInfo, err := bootstrapper.Get()
bs := NewHTTPBootstrapper(":", svr.URL)
ctx, cancel := context.WithCancel(t.Context())
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
return bs.Run(gCtx, "")
})
addrInfos, err := bs.Get(t.Context())
require.NoError(t, err)
require.Len(t, addrInfos, 1)
addrInfo := addrInfos[0]
require.Len(t, addrInfo.Addrs, 1)
require.Equal(t, "/ip4/104.131.131.82/tcp/4001", addrInfo.Addrs[0].String())
require.Equal(t, "QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", addrInfo.ID.String())
cancel()
err = g.Wait()
require.NoError(t, err)
}

80
pkg/routing/memory.go Normal file
View File

@ -0,0 +1,80 @@
package routing
import (
"context"
"net/netip"
"slices"
"sync"
)
var _ Router = &MemoryRouter{}
type MemoryRouter struct {
resolver map[string][]netip.AddrPort
self netip.AddrPort
mx sync.RWMutex
}
func NewMemoryRouter(resolver map[string][]netip.AddrPort, self netip.AddrPort) *MemoryRouter {
return &MemoryRouter{
resolver: resolver,
self: self,
}
}
func (m *MemoryRouter) Ready(ctx context.Context) (bool, error) {
m.mx.RLock()
defer m.mx.RUnlock()
return len(m.resolver) > 0, nil
}
func (m *MemoryRouter) Resolve(ctx context.Context, key string, count int) (<-chan netip.AddrPort, error) {
m.mx.RLock()
peers, ok := m.resolver[key]
m.mx.RUnlock()
peerCh := make(chan netip.AddrPort, count)
// If no peers exist close the channel to stop any consumer.
if !ok {
close(peerCh)
return peerCh, nil
}
go func() {
for _, peer := range peers {
peerCh <- peer
}
close(peerCh)
}()
return peerCh, nil
}
func (m *MemoryRouter) Advertise(ctx context.Context, keys []string) error {
for _, key := range keys {
m.Add(key, m.self)
}
return nil
}
func (m *MemoryRouter) Add(key string, ap netip.AddrPort) {
m.mx.Lock()
defer m.mx.Unlock()
v, ok := m.resolver[key]
if !ok {
m.resolver[key] = []netip.AddrPort{ap}
return
}
if slices.Contains(v, ap) {
return
}
m.resolver[key] = append(v, ap)
}
func (m *MemoryRouter) Lookup(key string) ([]netip.AddrPort, bool) {
m.mx.RLock()
defer m.mx.RUnlock()
v, ok := m.resolver[key]
return v, ok
}

View File

@ -0,0 +1,47 @@
package routing
import (
"net/netip"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestMemoryRouter(t *testing.T) {
t.Parallel()
r := NewMemoryRouter(map[string][]netip.AddrPort{}, netip.AddrPort{})
isReady, err := r.Ready(t.Context())
require.NoError(t, err)
require.False(t, isReady)
err = r.Advertise(t.Context(), []string{"foo"})
require.NoError(t, err)
isReady, err = r.Ready(t.Context())
require.NoError(t, err)
require.True(t, isReady)
r.Add("foo", netip.MustParseAddrPort("127.0.0.1:9090"))
peerCh, err := r.Resolve(t.Context(), "foo", 2)
require.NoError(t, err)
peers := []netip.AddrPort{}
for peer := range peerCh {
peers = append(peers, peer)
}
require.Len(t, peers, 2)
peers, ok := r.Lookup("foo")
require.True(t, ok)
require.Len(t, peers, 2)
peerCh, err = r.Resolve(t.Context(), "bar", 1)
require.NoError(t, err)
time.Sleep(1 * time.Second)
select {
case <-peerCh:
default:
t.Error("expected peer channel to be closed")
}
_, ok = r.Lookup("bar")
require.False(t, ok)
}

View File

@ -1,60 +0,0 @@
package routing
import (
"context"
"net/netip"
"sync"
)
type MockRouter struct {
resolver map[string][]netip.AddrPort
self netip.AddrPort
mx sync.RWMutex
}
func NewMockRouter(resolver map[string][]netip.AddrPort, self netip.AddrPort) *MockRouter {
return &MockRouter{
resolver: resolver,
self: self,
}
}
func (m *MockRouter) Ready() (bool, error) {
m.mx.RLock()
defer m.mx.RUnlock()
return len(m.resolver) > 0, nil
}
func (m *MockRouter) Resolve(ctx context.Context, key string, allowSelf bool, count int) (<-chan netip.AddrPort, error) {
peerCh := make(chan netip.AddrPort, count)
peers, ok := m.resolver[key]
// Not found will look forever until timeout.
if !ok {
return peerCh, nil
}
go func() {
m.mx.RLock()
defer m.mx.RUnlock()
for _, peer := range peers {
peerCh <- peer
}
close(peerCh)
}()
return peerCh, nil
}
func (m *MockRouter) Advertise(ctx context.Context, keys []string) error {
m.mx.Lock()
defer m.mx.Unlock()
for _, key := range keys {
m.resolver[key] = []netip.AddrPort{m.self}
}
return nil
}
func (m *MockRouter) LookupKey(key string) ([]netip.AddrPort, bool) {
m.mx.RLock()
defer m.mx.RUnlock()
v, ok := m.resolver[key]
return v, ok
}

View File

@ -2,10 +2,16 @@ package routing
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"net/netip"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@ -14,8 +20,10 @@ import (
cid "github.com/ipfs/go-cid"
"github.com/libp2p/go-libp2p"
dht "github.com/libp2p/go-libp2p-kad-dht"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/sec"
"github.com/libp2p/go-libp2p/p2p/discovery/routing"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
@ -28,6 +36,41 @@ import (
const KeyTTL = 10 * time.Minute
type P2PRouterConfig struct {
DataDir string
Libp2pOpts []libp2p.Option
}
func (cfg *P2PRouterConfig) Apply(opts ...P2PRouterOption) error {
for _, opt := range opts {
if opt == nil {
continue
}
if err := opt(cfg); err != nil {
return err
}
}
return nil
}
type P2PRouterOption func(cfg *P2PRouterConfig) error
func WithLibP2POptions(opts ...libp2p.Option) P2PRouterOption {
return func(cfg *P2PRouterConfig) error {
cfg.Libp2pOpts = opts
return nil
}
}
func WithDataDir(dataDir string) P2PRouterOption {
return func(cfg *P2PRouterConfig) error {
cfg.DataDir = dataDir
return nil
}
}
var _ Router = &P2PRouter{}
type P2PRouter struct {
bootstrapper Bootstrapper
host host.Host
@ -36,7 +79,13 @@ type P2PRouter struct {
registryPort uint16
}
func NewP2PRouter(ctx context.Context, addr string, bootstrapper Bootstrapper, registryPortStr string, opts ...libp2p.Option) (*P2PRouter, error) {
func NewP2PRouter(ctx context.Context, addr string, bs Bootstrapper, registryPortStr string, opts ...P2PRouterOption) (*P2PRouter, error) {
cfg := P2PRouterConfig{}
err := cfg.Apply(opts...)
if err != nil {
return nil, err
}
registryPort, err := strconv.ParseUint(registryPortStr, 10, 16)
if err != nil {
return nil, err
@ -66,12 +115,20 @@ func NewP2PRouter(ctx context.Context, addr string, bootstrapper Bootstrapper, r
}
return nil
})
opts = append(opts,
libp2pOpts := []libp2p.Option{
libp2p.ListenAddrs(multiAddrs...),
libp2p.PrometheusRegisterer(metrics.DefaultRegisterer),
addrFactoryOpt,
)
host, err := libp2p.New(opts...)
}
if cfg.DataDir != "" {
peerKey, err := loadOrCreatePrivateKey(ctx, cfg.DataDir)
if err != nil {
return nil, err
}
libp2pOpts = append(libp2pOpts, libp2p.Identity(peerKey))
}
libp2pOpts = append(libp2pOpts, cfg.Libp2pOpts...)
host, err := libp2p.New(libp2pOpts...)
if err != nil {
return nil, fmt.Errorf("could not create host: %w", err)
}
@ -83,25 +140,12 @@ func NewP2PRouter(ctx context.Context, addr string, bootstrapper Bootstrapper, r
return nil, fmt.Errorf("expected single host address but got %d %s", len(addrs), strings.Join(addrs, ", "))
}
log := logr.FromContextOrDiscard(ctx).WithName("p2p")
bootstrapPeerOpt := dht.BootstrapPeersFunc(func() []peer.AddrInfo {
addrInfo, err := bootstrapper.Get()
if err != nil {
log.Error(err, "could not get bootstrap addresses")
return nil
}
if addrInfo.ID == host.ID() {
log.Info("leader is self skipping connection to bootstrap node")
return nil
}
return []peer.AddrInfo{*addrInfo}
})
dhtOpts := []dht.Option{
dht.Mode(dht.ModeServer),
dht.ProtocolPrefix("/spegel"),
dht.DisableValues(),
dht.MaxRecordAge(KeyTTL),
bootstrapPeerOpt,
dht.BootstrapPeersFunc(bootstrapFunc(ctx, bs, host)),
}
kdht, err := dht.New(ctx, host, dhtOpts...)
if err != nil {
@ -110,7 +154,7 @@ func NewP2PRouter(ctx context.Context, addr string, bootstrapper Bootstrapper, r
rd := routing.NewRoutingDiscovery(kdht)
return &P2PRouter{
bootstrapper: bootstrapper,
bootstrapper: bs,
host: host,
kdht: kdht,
rd: rd,
@ -118,38 +162,53 @@ func NewP2PRouter(ctx context.Context, addr string, bootstrapper Bootstrapper, r
}, nil
}
func (r *P2PRouter) Run(ctx context.Context) error {
func (r *P2PRouter) Run(ctx context.Context) (err error) {
self := fmt.Sprintf("%s/p2p/%s", r.host.Addrs()[0].String(), r.host.ID().String())
logr.FromContextOrDiscard(ctx).WithName("p2p").Info("starting p2p router", "id", self)
if err := r.kdht.Bootstrap(ctx); err != nil {
return fmt.Errorf("could not boostrap distributed hash table: %w", err)
return fmt.Errorf("could not bootstrap distributed hash table: %w", err)
}
err := r.bootstrapper.Run(ctx, self)
defer func() {
cerr := r.host.Close()
if cerr != nil {
err = errors.Join(err, cerr)
}
}()
err = r.bootstrapper.Run(ctx, self)
if err != nil {
return err
}
return nil
}
func (r *P2PRouter) Close() error {
return r.host.Close()
}
func (r *P2PRouter) Ready() (bool, error) {
addrInfo, err := r.bootstrapper.Get()
func (r *P2PRouter) Ready(ctx context.Context) (bool, error) {
addrInfos, err := r.bootstrapper.Get(ctx)
if err != nil {
return false, err
}
if addrInfo.ID == r.host.ID() {
return true, nil
}
if r.kdht.RoutingTable().Size() == 0 {
if len(addrInfos) == 0 {
return false, nil
}
return true, nil
if len(addrInfos) == 1 {
matches, err := hostMatches(*host.InfoFromHost(r.host), addrInfos[0])
if err != nil {
return false, err
}
if matches {
return true, nil
}
}
if r.kdht.RoutingTable().Size() > 0 {
return true, nil
}
err = r.kdht.Bootstrap(ctx)
if err != nil {
return false, err
}
return false, nil
}
func (r *P2PRouter) Resolve(ctx context.Context, key string, allowSelf bool, count int) (<-chan netip.AddrPort, error) {
func (r *P2PRouter) Resolve(ctx context.Context, key string, count int) (<-chan netip.AddrPort, error) {
log := logr.FromContextOrDiscard(ctx).WithValues("host", r.host.ID().String(), "key", key)
c, err := createCid(key)
if err != nil {
@ -161,34 +220,36 @@ func (r *P2PRouter) Resolve(ctx context.Context, key string, allowSelf bool, cou
if peerBufferSize == 0 {
peerBufferSize = 20
}
addrCh := r.rd.FindProvidersAsync(ctx, c, count)
addrInfoCh := r.rd.FindProvidersAsync(ctx, c, count)
peerCh := make(chan netip.AddrPort, peerBufferSize)
go func() {
resolveTimer := prometheus.NewTimer(metrics.ResolveDurHistogram.WithLabelValues("libp2p"))
for info := range addrCh {
for addrInfo := range addrInfoCh {
resolveTimer.ObserveDuration()
if !allowSelf && info.ID == r.host.ID() {
continue
}
if len(info.Addrs) != 1 {
if len(addrInfo.Addrs) != 1 {
addrs := []string{}
for _, addr := range info.Addrs {
for _, addr := range addrInfo.Addrs {
addrs = append(addrs, addr.String())
}
log.Info("expected address list to only contain a single item", "addresses", strings.Join(addrs, ", "))
continue
}
ipAddr, err := ipInMultiaddr(info.Addrs[0])
ip, err := manet.ToIP(addrInfo.Addrs[0])
if err != nil {
log.Error(err, "could not get IP address")
continue
}
ipAddr, ok := netip.AddrFromSlice(ip)
if !ok {
log.Error(errors.New("IP is not IPV4 or IPV6"), "could not convert IP")
continue
}
peer := netip.AddrPortFrom(ipAddr, r.registryPort)
// Don't block if the client has disconnected before reading all values from the channel
select {
case peerCh <- peer:
default:
log.V(10).Info("mirror endpoint dropped: peer channel is full")
log.V(4).Info("mirror endpoint dropped: peer channel is full")
}
}
close(peerCh)
@ -197,7 +258,7 @@ func (r *P2PRouter) Resolve(ctx context.Context, key string, allowSelf bool, cou
}
func (r *P2PRouter) Advertise(ctx context.Context, keys []string) error {
logr.FromContextOrDiscard(ctx).V(10).Info("advertising keys", "host", r.host.ID().String(), "keys", keys)
logr.FromContextOrDiscard(ctx).V(4).Info("advertising keys", "host", r.host.ID().String(), "keys", keys)
for _, key := range keys {
c, err := createCid(key)
if err != nil {
@ -211,6 +272,86 @@ func (r *P2PRouter) Advertise(ctx context.Context, keys []string) error {
return nil
}
func bootstrapFunc(ctx context.Context, bootstrapper Bootstrapper, h host.Host) func() []peer.AddrInfo {
log := logr.FromContextOrDiscard(ctx).WithName("p2p")
return func() []peer.AddrInfo {
bootstrapCtx, bootstrapCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer bootstrapCancel()
// TODO (phillebaba): Consider if we should do a best effort bootstrap without host address.
hostAddrs := h.Addrs()
if len(hostAddrs) == 0 {
return nil
}
var hostPort ma.Component
ma.ForEach(hostAddrs[0], func(c ma.Component) bool {
if c.Protocol().Code == ma.P_TCP {
hostPort = c
return false
}
return true
})
addrInfos, err := bootstrapper.Get(bootstrapCtx)
if err != nil {
log.Error(err, "could not get bootstrap addresses")
return nil
}
filteredAddrInfos := []peer.AddrInfo{}
for _, addrInfo := range addrInfos {
// Skip addresses that match host.
matches, err := hostMatches(*host.InfoFromHost(h), addrInfo)
if err != nil {
log.Error(err, "could not compare host with address")
continue
}
if matches {
log.Info("skipping bootstrap peer that is same as host")
continue
}
// Add port to address if it is missing.
modifiedAddrs := []ma.Multiaddr{}
for _, addr := range addrInfo.Addrs {
hasPort := false
ma.ForEach(addr, func(c ma.Component) bool {
if c.Protocol().Code == ma.P_TCP {
hasPort = true
return false
}
return true
})
if hasPort {
modifiedAddrs = append(modifiedAddrs, addr)
continue
}
modifiedAddrs = append(modifiedAddrs, ma.Join(addr, &hostPort))
}
addrInfo.Addrs = modifiedAddrs
// Resolve ID if it is missing.
if addrInfo.ID != "" {
filteredAddrInfos = append(filteredAddrInfos, addrInfo)
continue
}
addrInfo.ID = "id"
err = h.Connect(bootstrapCtx, addrInfo)
var mismatchErr sec.ErrPeerIDMismatch
if !errors.As(err, &mismatchErr) {
log.Error(err, "could not get peer id")
continue
}
addrInfo.ID = mismatchErr.Actual
filteredAddrInfos = append(filteredAddrInfos, addrInfo)
}
if len(filteredAddrInfos) == 0 {
log.Info("no bootstrap nodes found")
return nil
}
return filteredAddrInfos
}
}
func listenMultiaddrs(addr string) ([]ma.Multiaddr, error) {
h, p, err := net.SplitHostPort(addr)
if err != nil {
@ -245,24 +386,6 @@ func listenMultiaddrs(addr string) ([]ma.Multiaddr, error) {
return multiAddrs, nil
}
func ipInMultiaddr(multiAddr ma.Multiaddr) (netip.Addr, error) {
for _, p := range []int{ma.P_IP6, ma.P_IP4} {
v, err := multiAddr.ValueForProtocol(p)
if errors.Is(err, ma.ErrProtocolNotFound) {
continue
}
if err != nil {
return netip.Addr{}, err
}
ipAddr, err := netip.ParseAddr(v)
if err != nil {
return netip.Addr{}, err
}
return ipAddr, nil
}
return netip.Addr{}, fmt.Errorf("IP not found in address")
}
func isIp6(m ma.Multiaddr) bool {
c, _ := ma.SplitFirst(m)
if c == nil || c.Protocol().Code != ma.P_IP6 {
@ -284,3 +407,83 @@ func createCid(key string) (cid.Cid, error) {
}
return c, nil
}
func hostMatches(host, addrInfo peer.AddrInfo) (bool, error) {
// Skip self when address ID matches host ID.
if host.ID != "" && addrInfo.ID != "" {
return host.ID == addrInfo.ID, nil
}
// Skip self when IP matches
hostIP, err := manet.ToIP(host.Addrs[0])
if err != nil {
return false, err
}
for _, addr := range addrInfo.Addrs {
addrIP, err := manet.ToIP(addr)
if err != nil {
return false, err
}
if hostIP.Equal(addrIP) {
return true, nil
}
}
return false, nil
}
func loadOrCreatePrivateKey(ctx context.Context, dataDir string) (crypto.PrivKey, error) {
keyPath := filepath.Join(dataDir, "private.key")
log := logr.FromContextOrDiscard(ctx).WithValues("path", keyPath)
err := os.MkdirAll(dataDir, 0o755)
if err != nil {
return nil, err
}
b, err := os.ReadFile(keyPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
}
if errors.Is(err, os.ErrNotExist) {
log.Info("creating a new private key")
privKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil {
return nil, err
}
rawBytes, err := privKey.Raw()
if err != nil {
return nil, err
}
pkcs8Bytes, err := x509.MarshalPKCS8PrivateKey(ed25519.PrivateKey(rawBytes))
if err != nil {
return nil, err
}
block := &pem.Block{
Type: "PRIVATE KEY",
Bytes: pkcs8Bytes,
}
pemData := pem.EncodeToMemory(block)
err = os.WriteFile(keyPath, pemData, 0o600)
if err != nil {
return nil, err
}
return privKey, nil
}
log.Info("loading the private key from data directory")
block, _ := pem.Decode(b)
if block == nil || block.Type != "PRIVATE KEY" {
return nil, fmt.Errorf("invalid PEM block type %s", block.Type)
}
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
edKey, ok := parsedKey.(ed25519.PrivateKey)
if !ok {
return nil, errors.New("not an Ed25519 private key")
}
privKey, err := crypto.UnmarshalEd25519PrivateKey(edKey)
if err != nil {
return nil, err
}
return privKey, nil
}

View File

@ -1,14 +1,184 @@
package routing
import (
"net/netip"
"context"
"fmt"
"testing"
"time"
"github.com/go-logr/logr"
tlog "github.com/go-logr/logr/testing"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
mocknet "github.com/libp2p/go-libp2p/p2p/net/mock"
ma "github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)
func TestP2PRouterOptions(t *testing.T) {
t.Parallel()
libp2pOpts := []libp2p.Option{
libp2p.ListenAddrStrings("foo"),
}
opts := []P2PRouterOption{
WithLibP2POptions(libp2pOpts...),
WithDataDir("foobar"),
}
cfg := P2PRouterConfig{}
err := cfg.Apply(opts...)
require.NoError(t, err)
require.Equal(t, libp2pOpts, cfg.Libp2pOpts)
require.Equal(t, "foobar", cfg.DataDir)
}
func TestP2PRouter(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(t.Context())
bs := NewStaticBootstrapper(nil)
router, err := NewP2PRouter(ctx, "localhost:0", bs, "9090")
require.NoError(t, err)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
return router.Run(gCtx)
})
// TODO (phillebaba): There is a test flake that sometime occurs sometimes if code runs too fast.
// Flake results in a peer being returned without an address. Revisit in Go 1.24 to see if this can be solved better.
time.Sleep(1 * time.Second)
err = router.Advertise(ctx, nil)
require.NoError(t, err)
peerCh, err := router.Resolve(ctx, "foo", 1)
require.NoError(t, err)
peer := <-peerCh
require.False(t, peer.IsValid())
err = router.Advertise(ctx, []string{"foo"})
require.NoError(t, err)
peerCh, err = router.Resolve(ctx, "foo", 1)
require.NoError(t, err)
peer = <-peerCh
require.True(t, peer.IsValid())
cancel()
err = g.Wait()
require.NoError(t, err)
}
func TestReady(t *testing.T) {
t.Parallel()
bs := NewStaticBootstrapper(nil)
router, err := NewP2PRouter(t.Context(), "localhost:0", bs, "9090")
require.NoError(t, err)
// Should not be ready if no peers are found.
isReady, err := router.Ready(t.Context())
require.NoError(t, err)
require.False(t, isReady)
// Should be ready if only peer is host.
bs.SetPeers([]peer.AddrInfo{*host.InfoFromHost(router.host)})
isReady, err = router.Ready(t.Context())
require.NoError(t, err)
require.True(t, isReady)
// Shouldd be not ready with multiple peers but empty routing table.
bs.SetPeers([]peer.AddrInfo{{}, {}})
isReady, err = router.Ready(t.Context())
require.NoError(t, err)
require.False(t, isReady)
// Should be ready with multiple peers and populated routing table.
newPeer, err := router.kdht.RoutingTable().GenRandPeerID(0)
require.NoError(t, err)
ok, err := router.kdht.RoutingTable().TryAddPeer(newPeer, false, false)
require.NoError(t, err)
require.True(t, ok)
bs.SetPeers([]peer.AddrInfo{{}, {}})
isReady, err = router.Ready(t.Context())
require.NoError(t, err)
require.True(t, isReady)
}
func TestBootstrapFunc(t *testing.T) {
t.Parallel()
log := tlog.NewTestLogger(t)
ctx := logr.NewContext(t.Context(), log)
mn, err := mocknet.WithNPeers(2)
require.NoError(t, err)
tests := []struct {
name string
peers []peer.AddrInfo
expected []string
}{
{
name: "no peers",
peers: []peer.AddrInfo{},
expected: []string{},
},
{
name: "nothing missing",
peers: []peer.AddrInfo{
{
ID: "foo",
Addrs: []ma.Multiaddr{ma.StringCast("/ip4/192.168.1.1/tcp/8080")},
},
},
expected: []string{"/ip4/192.168.1.1/tcp/8080/p2p/foo"},
},
{
name: "only self",
peers: []peer.AddrInfo{
{
ID: mn.Hosts()[0].ID(),
Addrs: []ma.Multiaddr{ma.StringCast("/ip4/192.168.1.1/tcp/8080")},
},
},
expected: []string{},
},
{
name: "missing port",
peers: []peer.AddrInfo{
{
ID: "foo",
Addrs: []ma.Multiaddr{ma.StringCast("/ip4/192.168.1.1")},
},
},
expected: []string{"/ip4/192.168.1.1/tcp/4242/p2p/foo"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
bs := NewStaticBootstrapper(tt.peers)
f := bootstrapFunc(ctx, bs, mn.Hosts()[0])
peers := f()
peerStrs := []string{}
for _, p := range peers {
id, err := p.ID.Marshal()
require.NoError(t, err)
peerStrs = append(peerStrs, fmt.Sprintf("%s/p2p/%s", p.Addrs[0].String(), string(id)))
}
require.ElementsMatch(t, tt.expected, peerStrs)
})
}
}
func TestListenMultiaddrs(t *testing.T) {
t.Parallel()
tests := []struct {
name string
addr string
@ -32,8 +202,11 @@ func TestListenMultiaddrs(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
multiAddrs, err := listenMultiaddrs(tt.addr)
require.NoError(t, err)
//nolint: testifylint // This is easier to read and understand.
require.Equal(t, len(tt.expected), len(multiAddrs))
for i, e := range tt.expected {
require.Equal(t, e, multiAddrs[i].String())
@ -42,35 +215,9 @@ func TestListenMultiaddrs(t *testing.T) {
}
}
func TestIPInMultiaddr(t *testing.T) {
tests := []struct {
ma string
expected netip.Addr
name string
}{
{
name: "ipv4",
ma: "/ip4/10.244.1.2/tcp/5001",
expected: netip.MustParseAddr("10.244.1.2"),
},
{
name: "ipv6",
ma: "/ip6/0:0:0:0:0:ffff:0af4:0102/tcp/5001",
expected: netip.MustParseAddr("::ffff:10.244.1.2"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
multiAddr, err := ma.NewMultiaddr(tt.ma)
require.NoError(t, err)
v, err := ipInMultiaddr(multiAddr)
require.NoError(t, err)
require.Equal(t, tt.expected, v)
})
}
}
func TestIsIp6(t *testing.T) {
t.Parallel()
m, err := ma.NewMultiaddr("/ip6/::")
require.NoError(t, err)
require.True(t, isIp6(m))
@ -78,3 +225,110 @@ func TestIsIp6(t *testing.T) {
require.NoError(t, err)
require.False(t, isIp6(m))
}
func TestCreateCid(t *testing.T) {
t.Parallel()
c, err := createCid("foobar")
require.NoError(t, err)
require.Equal(t, "bafkreigdvoh7cnza5cwzar65hfdgwpejotszfqx2ha6uuolaofgk54ge6i", c.String())
}
func TestHostMatches(t *testing.T) {
t.Parallel()
tests := []struct {
name string
host peer.AddrInfo
addrInfo peer.AddrInfo
expected bool
}{
{
name: "ID match",
host: peer.AddrInfo{
ID: "foo",
Addrs: []ma.Multiaddr{},
},
addrInfo: peer.AddrInfo{
ID: "foo",
Addrs: []ma.Multiaddr{},
},
expected: true,
},
{
name: "ID do not match",
host: peer.AddrInfo{
ID: "foo",
Addrs: []ma.Multiaddr{},
},
addrInfo: peer.AddrInfo{
ID: "bar",
Addrs: []ma.Multiaddr{},
},
expected: false,
},
{
name: "IP4 match",
host: peer.AddrInfo{
ID: "",
Addrs: []ma.Multiaddr{ma.StringCast("/ip4/192.168.1.1")},
},
addrInfo: peer.AddrInfo{
ID: "",
Addrs: []ma.Multiaddr{ma.StringCast("/ip4/192.168.1.1")},
},
expected: true,
},
{
name: "IP4 do not match",
host: peer.AddrInfo{
ID: "",
Addrs: []ma.Multiaddr{ma.StringCast("/ip4/192.168.1.1")},
},
addrInfo: peer.AddrInfo{
ID: "",
Addrs: []ma.Multiaddr{ma.StringCast("/ip4/192.168.1.2")},
},
expected: false,
},
{
name: "IP6 match",
host: peer.AddrInfo{
ID: "",
Addrs: []ma.Multiaddr{ma.StringCast("/ip6/c3c9:152b:73d1:dad0:e2f9:a521:6356:88ba")},
},
addrInfo: peer.AddrInfo{
ID: "",
Addrs: []ma.Multiaddr{ma.StringCast("/ip6/c3c9:152b:73d1:dad0:e2f9:a521:6356:88ba")},
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
matches, err := hostMatches(tt.host, tt.addrInfo)
require.NoError(t, err)
require.Equal(t, tt.expected, matches)
})
}
}
func TestLoadOrCreatePrivateKey(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
data := []byte("hello world")
firstPrivKey, err := loadOrCreatePrivateKey(t.Context(), tmpDir)
require.NoError(t, err)
sig, err := firstPrivKey.Sign(data)
require.NoError(t, err)
secondPrivKey, err := loadOrCreatePrivateKey(t.Context(), tmpDir)
require.NoError(t, err)
ok, err := secondPrivKey.GetPublic().Verify(data, sig)
require.NoError(t, err)
require.True(t, ok)
require.True(t, firstPrivKey.Equals(secondPrivKey))
}

View File

@ -5,8 +5,12 @@ import (
"net/netip"
)
// Router implements the discovery of content.
type Router interface {
Ready() (bool, error)
Resolve(ctx context.Context, key string, allowSelf bool, count int) (<-chan netip.AddrPort, error)
// Ready returns true when the router is ready.
Ready(ctx context.Context) (bool, error)
// Resolve asynchronously discovers addresses that can serve the content defined by the give key.
Resolve(ctx context.Context, key string, count int) (<-chan netip.AddrPort, error)
// Advertise broadcasts that the current router can serve the content.
Advertise(ctx context.Context, keys []string) error
}

View File

@ -3,7 +3,6 @@ package state
import (
"context"
"errors"
"fmt"
"time"
"github.com/go-logr/logr"
@ -14,114 +13,107 @@ import (
"github.com/spegel-org/spegel/pkg/routing"
)
func Track(ctx context.Context, ociClient oci.Client, router routing.Router, resolveLatestTag bool) error {
func Track(ctx context.Context, ociStore oci.Store, router routing.Router, resolveLatestTag bool) error {
log := logr.FromContextOrDiscard(ctx)
eventCh, errCh := ociClient.Subscribe(ctx)
immediate := make(chan time.Time, 1)
immediate <- time.Now()
eventCh, err := ociStore.Subscribe(ctx)
if err != nil {
return err
}
immediateCh := make(chan time.Time, 1)
immediateCh <- time.Now()
close(immediateCh)
expirationTicker := time.NewTicker(routing.KeyTTL - time.Minute)
defer expirationTicker.Stop()
ticker := channel.Merge(immediate, expirationTicker.C)
tickerCh := channel.Merge(immediateCh, expirationTicker.C)
for {
select {
case <-ctx.Done():
return nil
case <-ticker:
log.Info("running scheduled image state update")
if err := all(ctx, ociClient, router, resolveLatestTag); err != nil {
case <-tickerCh:
log.Info("running state update")
err := tick(ctx, ociStore, router, resolveLatestTag)
if err != nil {
log.Error(err, "received errors when updating all images")
continue
}
case event, ok := <-eventCh:
if !ok {
return errors.New("image event channel closed")
return errors.New("event channel closed")
}
log.Info("received image event", "image", event.Image, "type", event.Type)
if _, err := update(ctx, ociClient, router, event, false, resolveLatestTag); err != nil {
log.Error(err, "received error when updating image")
log.Info("OCI event", "key", event.Key, "type", event.Type)
err := handle(ctx, router, event)
if err != nil {
log.Error(err, "could not handle event")
continue
}
case err, ok := <-errCh:
if !ok {
return errors.New("image error channel closed")
}
log.Error(err, "event channel error")
}
}
}
func all(ctx context.Context, ociClient oci.Client, router routing.Router, resolveLatestTag bool) error {
log := logr.FromContextOrDiscard(ctx).V(5)
imgs, err := ociClient.ListImages(ctx)
func tick(ctx context.Context, ociStore oci.Store, router routing.Router, resolveLatest bool) error {
advertisedImages := map[string]float64{}
advertisedImageDigests := map[string]float64{}
advertisedImageTags := map[string]float64{}
advertisedKeys := map[string]float64{}
imgs, err := ociStore.ListImages(ctx)
if err != nil {
return err
}
// TODO: Update metrics on subscribed events. This will require keeping state in memory to know about key count changes.
metrics.AdvertisedKeys.Reset()
metrics.AdvertisedImages.Reset()
metrics.AdvertisedImageTags.Reset()
metrics.AdvertisedImageDigests.Reset()
errs := []error{}
targets := map[string]interface{}{}
for _, img := range imgs {
_, skipDigests := targets[img.Digest.String()]
// Handle the list re-sync as update events; this will also prevent the
// update function from setting metrics values.
event := oci.ImageEvent{Image: img, Type: oci.UpdateEvent}
log.Info("sync image event", "image", event.Image, "type", event.Type)
keyTotal, err := update(ctx, ociClient, router, event, skipDigests, resolveLatestTag)
if err != nil {
errs = append(errs, err)
advertisedImages[img.Registry] += 1
advertisedImageDigests[img.Registry] += 1
if !resolveLatest && img.IsLatestTag() {
continue
}
targets[img.Digest.String()] = nil
metrics.AdvertisedKeys.WithLabelValues(img.Registry).Add(float64(keyTotal))
metrics.AdvertisedImages.WithLabelValues(img.Registry).Add(1)
if img.Tag == "" {
metrics.AdvertisedImageDigests.WithLabelValues(event.Image.Registry).Add(1)
} else {
metrics.AdvertisedImageTags.WithLabelValues(event.Image.Registry).Add(1)
tagName, ok := img.TagName()
if !ok {
continue
}
err := router.Advertise(ctx, []string{tagName})
if err != nil {
return err
}
advertisedImageTags[img.Registry] += 1
advertisedKeys[img.Registry] += 1
}
contents, err := ociStore.ListContents(ctx)
if err != nil {
return err
}
for _, content := range contents {
err := router.Advertise(ctx, []string{content.Digest.String()})
if err != nil {
return err
}
for _, registry := range content.Registires {
advertisedKeys[registry] += 1
}
}
return errors.Join(errs...)
for k, v := range advertisedImages {
metrics.AdvertisedImages.WithLabelValues(k).Set(v)
}
for k, v := range advertisedImageDigests {
metrics.AdvertisedImageDigests.WithLabelValues(k).Set(v)
}
for k, v := range advertisedImageTags {
metrics.AdvertisedImageTags.WithLabelValues(k).Set(v)
}
for k, v := range advertisedKeys {
metrics.AdvertisedKeys.WithLabelValues(k).Set(v)
}
return nil
}
func update(ctx context.Context, ociClient oci.Client, router routing.Router, event oci.ImageEvent, skipDigests, resolveLatestTag bool) (int, error) {
keys := []string{}
if !(!resolveLatestTag && event.Image.IsLatestTag()) {
if tagRef, ok := event.Image.TagName(); ok {
keys = append(keys, tagRef)
}
func handle(ctx context.Context, router routing.Router, event oci.OCIEvent) error {
if event.Type != oci.CreateEvent {
return nil
}
if event.Type == oci.DeleteEvent {
// We don't know how many digest keys were associated with the deleted image;
// that can only be updated by the full image list sync in all().
metrics.AdvertisedImages.WithLabelValues(event.Image.Registry).Sub(1)
// DHT doesn't actually have any way to stop providing a key, you just have to wait for the record to expire
// from the datastore. Record TTL is a datastore-level value, so we can't even re-provide with a shorter TTL.
return 0, nil
}
if !skipDigests {
dgsts, err := ociClient.AllIdentifiers(ctx, event.Image)
if err != nil {
return 0, fmt.Errorf("could not get digests for image %s: %w", event.Image.String(), err)
}
keys = append(keys, dgsts...)
}
err := router.Advertise(ctx, keys)
err := router.Advertise(ctx, []string{event.Key})
if err != nil {
return 0, fmt.Errorf("could not advertise image %s: %w", event.Image.String(), err)
return err
}
if event.Type == oci.CreateEvent {
// We don't know how many unique digest keys will be associated with the new image;
// that can only be updated by the full image list sync in all().
metrics.AdvertisedImages.WithLabelValues(event.Image.Registry).Add(1)
if event.Image.Tag == "" {
metrics.AdvertisedImageDigests.WithLabelValues(event.Image.Registry).Add(1)
} else {
metrics.AdvertisedImageTags.WithLabelValues(event.Image.Registry).Add(1)
}
}
return len(keys), nil
return nil
}

View File

@ -2,17 +2,60 @@ package state
import (
"context"
"crypto/sha256"
"encoding/json"
"math/rand/v2"
"net/netip"
"strconv"
"testing"
"time"
"golang.org/x/sync/errgroup"
"github.com/go-logr/logr"
tlog "github.com/go-logr/logr/testing"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/require"
"github.com/spegel-org/spegel/pkg/oci"
"github.com/spegel-org/spegel/pkg/routing"
)
func TestBasic(t *testing.T) {
func TestTrack(t *testing.T) {
t.Parallel()
ociStore := oci.NewMemory()
imgRefs := []string{
"docker.io/library/ubuntu:latest",
"ghcr.io/spegel-org/spegel:v0.0.9",
}
imgs := []oci.Image{}
for _, imageStr := range imgRefs {
manifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
MediaType: ocispec.MediaTypeImageManifest,
Annotations: map[string]string{
"random": strconv.Itoa(rand.Int()),
},
}
b, err := json.Marshal(&manifest)
require.NoError(t, err)
hash := sha256.New()
_, err = hash.Write(b)
require.NoError(t, err)
dgst := digest.NewDigest(digest.SHA256, hash)
ociStore.AddBlob(b, dgst)
img, err := oci.ParseImageRequireDigest(imageStr, dgst)
require.NoError(t, err)
ociStore.AddImage(img)
imgs = append(imgs, img)
}
tests := []struct {
name string
resolveLatestTag bool
@ -26,41 +69,30 @@ func TestBasic(t *testing.T) {
resolveLatestTag: false,
},
}
imgRefs := []string{
"docker.io/library/ubuntu:latest@sha256:b060fffe8e1561c9c3e6dea6db487b900100fc26830b9ea2ec966c151ab4c020",
"ghcr.io/spegel-org/spegel:v0.0.9@sha256:fa32bd3bcd49a45a62cfc1b0fed6a0b63bf8af95db5bad7ec22865aee0a4b795",
"docker.io/library/alpine@sha256:25fad2a32ad1f6f510e528448ae1ec69a28ef81916a004d3629874104f8a7f70",
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
imgs := []oci.Image{}
for _, imageStr := range imgRefs {
img, err := oci.Parse(imageStr, "")
require.NoError(t, err)
imgs = append(imgs, img)
}
ociClient := oci.NewMockClient(imgs)
router := routing.NewMockRouter(map[string][]netip.AddrPort{}, netip.MustParseAddrPort("127.0.0.1:5000"))
t.Parallel()
ctx, cancel := context.WithCancel(context.TODO())
go func() {
time.Sleep(2 * time.Second)
cancel()
}()
err := Track(ctx, ociClient, router, tt.resolveLatestTag)
require.NoError(t, err)
log := tlog.NewTestLogger(t)
ctx := logr.NewContext(t.Context(), log)
ctx, cancel := context.WithCancel(ctx)
router := routing.NewMemoryRouter(map[string][]netip.AddrPort{}, netip.MustParseAddrPort("127.0.0.1:5000"))
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
return Track(gCtx, ociStore, router, tt.resolveLatestTag)
})
time.Sleep(100 * time.Millisecond)
for _, img := range imgs {
peers, ok := router.LookupKey(img.Digest.String())
peers, ok := router.Lookup(img.Digest.String())
require.True(t, ok)
require.Len(t, peers, 1)
tagName, ok := img.TagName()
if !ok {
continue
}
peers, ok = router.LookupKey(tagName)
peers, ok = router.Lookup(tagName)
if img.IsLatestTag() && !tt.resolveLatestTag {
require.False(t, ok)
continue
@ -68,6 +100,10 @@ func TestBasic(t *testing.T) {
require.True(t, ok)
require.Len(t, peers, 1)
}
cancel()
err := g.Wait()
require.NoError(t, err)
})
}
}

View File

@ -1,48 +0,0 @@
package throttle
import (
"fmt"
"regexp"
"strconv"
)
var unmarshalRegex = regexp.MustCompile(`^(\d+)\s?([KMGT]?Bps)$`)
type Byterate int64
const (
Bps Byterate = 1
KBps = 1024 * Bps
MBps = 1024 * KBps
GBps = 1024 * MBps
TBps = 1024 * GBps
)
func (br *Byterate) UnmarshalText(b []byte) error {
comps := unmarshalRegex.FindStringSubmatch(string(b))
if len(comps) != 3 {
return fmt.Errorf("invalid byterate format %s should be n Bps, n KBps, n MBps, n GBps, or n TBps", string(b))
}
v, err := strconv.Atoi(comps[1])
if err != nil {
return err
}
unitStr := comps[2]
var unit Byterate
switch unitStr {
case "Bps":
unit = Bps
case "KBps":
unit = KBps
case "MBps":
unit = MBps
case "GBps":
unit = GBps
case "TBps":
unit = TBps
default:
return fmt.Errorf("unknown unit %s", unitStr)
}
*br = Byterate(v) * unit
return nil
}

View File

@ -1,67 +0,0 @@
package throttle
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestByterateUnmarshalValid(t *testing.T) {
tests := []struct {
input string
expected Byterate
}{
{
input: "1 Bps",
expected: 1 * Bps,
},
{
input: "31 KBps",
expected: 31 * KBps,
},
{
input: "42 MBps",
expected: 42 * MBps,
},
{
input: "120 GBps",
expected: 120 * GBps,
},
{
input: "3TBps",
expected: 3 * TBps,
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
var br Byterate
err := br.UnmarshalText([]byte(tt.input))
require.NoError(t, err)
require.Equal(t, tt.expected, br)
})
}
}
func TestByterateUnmarshalInvalid(t *testing.T) {
tests := []struct {
input string
}{
{
input: "foobar",
},
{
input: "1 Mbps",
},
{
input: "1.1 MBps",
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
var br Byterate
err := br.UnmarshalText([]byte(tt.input))
require.EqualError(t, err, fmt.Sprintf("invalid byterate format %s should be n Bps, n KBps, n MBps, n GBps, or n TBps", tt.input))
})
}
}

View File

@ -1,48 +0,0 @@
package throttle
import (
"fmt"
"io"
"time"
"golang.org/x/time/rate"
)
const burstLimit = 1024 * 1024 * 1024 // 1GB
type Throttler struct {
limiter *rate.Limiter
}
func NewThrottler(br Byterate) *Throttler {
limiter := rate.NewLimiter(rate.Limit(br), burstLimit)
limiter.AllowN(time.Now(), burstLimit)
return &Throttler{
limiter: limiter,
}
}
func (t *Throttler) Writer(w io.Writer) io.Writer {
return &writer{
limiter: t.limiter,
writer: w,
}
}
type writer struct {
limiter *rate.Limiter
writer io.Writer
}
func (w *writer) Write(p []byte) (int, error) {
n, err := w.writer.Write(p)
if err != nil {
return 0, err
}
r := w.limiter.ReserveN(time.Now(), n)
if !r.OK() {
return n, fmt.Errorf("write size %d exceeds limiters burst %d", n, w.limiter.Burst())
}
time.Sleep(r.Delay())
return n, nil
}

View File

@ -1,26 +0,0 @@
package throttle
import (
"bytes"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestThrottler(t *testing.T) {
br := 500 * Bps
throttler := NewThrottler(br)
w := throttler.Writer(bytes.NewBuffer([]byte{}))
chunkSize := 100
start := time.Now()
for i := 0; i < 10; i++ {
b := make([]byte, chunkSize)
n, err := w.Write(b)
require.NoError(t, err)
require.Equal(t, chunkSize, n)
}
d := time.Since(start)
require.Greater(t, d, 2*time.Second)
require.Less(t, d, 3*time.Second)
}

View File

@ -1,36 +0,0 @@
benchmark.kubeconfig
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files, which are likely to contain sensitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
*.tfvars
*.tfvars.json
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc

View File

@ -1,52 +0,0 @@
# Benchmark
The benchmark measures image pull performance in realistic scenarios. The purpose is to validate the expected performance of Spegel and give an indication of the expected performance.
Spegel works best when deploying multiple replicas of an application, as the same image needs to be pulled to multiple nodes resulting in the image being pulled from Spegel.
For this reason, the benchmark consists of two steps. The first step will deploy a daemonset to the cluster. During this step all pods will be deployed at once and all images pulled at the same time.
It tests the worst condition for Spegel as none of the nodes will have the cached image creating a race to pull it first. However, as the image is pulled to the first node it will enable other nodes to pull the image.
The second step updates the daemonset with a new version of the image. Each pod will be replaced one at a time when updating a daemonset. This scenario is better for Spegel.
The first node will need to pull the image from the registry but the second will be able to pull the image from the first node.
In theory, both of these steps should result in a faster overall image pull time and a similar image pull time from the registry.
## Method
This method describes the process of running the benchmarks on an AKS cluster created with the accompanied Terraform. Replace the kubeconfig to run the benchmark on another cluster.
Start by creating the AKS cluster. A kubeconfig file will be created in the terraform directory after the AKS cluster has been successfully created.
```bash
cd terraform
terraform init
terraform apply
cd ..
```
Run the benchmark without Spegel installed. The first Nginx image is small and has a few layers while the second Plex image is a lot larger. The benchmark will output the path to a directory containing the results.
```bash
go run benchmark.go benchmark --result-dir ./results --name nginx-without-spegel --kubeconfig ./terraform/benchmark.kubeconfig --namespace spegel-benchmark --images ghcr.io/mirrorshub/docker/nginx:1.24-alpine ghcr.io/mirrorshub/docker/nginx:1.25-alpine
go run benchmark.go benchmark --result-dir ./results --name plex-without-spegel --kubeconfig ./terraform/benchmark.kubeconfig --namespace spegel-benchmark --images ghcr.io/linuxserver/plex:1.31.0 ghcr.io/linuxserver/plex:1.32.0
```
Deploy Spegel in the cluster and wait for all of the pods to run.
```bash
export KUBECONFIG=$(pwd)/terraform/benchmark.kubeconfig
helm upgrade --create-namespace --namespace spegel --install --version $VERSION spegel oci://ghcr.io/spegel-org/helm-charts/spegel
kubectl --namespace spegel rollout status daemonset spegel --timeout 60s
```
Run the same benchmarks as before, now with Spegel installed.
```bash
go run benchmark.go benchmark --result-dir ./results --name nginx-with-spegel --kubeconfig ./terraform/benchmark.kubeconfig --namespace spegel-benchmark --images ghcr.io/mirrorshub/docker/nginx:1.24-alpine ghcr.io/mirrorshub/docker/nginx:1.25-alpine
go run benchmark.go benchmark --result-dir ./results --name plex-with-spegel --kubeconfig ./terraform/benchmark.kubeconfig --namespace spegel-benchmark --images ghcr.io/linuxserver/plex:1.31.0 ghcr.io/linuxserver/plex:1.32.0
```
Destroy the AKS cluster as it is no longer needed.
```bash
cd terraform
terraform destroy
cd ..
```

View File

@ -1,454 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"os/signal"
"path"
"regexp"
"strings"
"syscall"
"time"
"image/color"
"github.com/alexflint/go-arg"
"golang.org/x/exp/slices"
"gonum.org/v1/plot"
"gonum.org/v1/plot/plotter"
"gonum.org/v1/plot/vg"
"gonum.org/v1/plot/vg/draw"
"gonum.org/v1/plot/vg/vgimg"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
)
type BenchmarkCmd struct {
ResultDir string `arg:"--result-dir,required"`
Name string `arg:"--name,required"`
KubeconfigPath string `arg:"--kubeconfig,required"`
Namespace string `arg:"--namespace,required"`
Images []string `arg:"--images,required"`
}
type AnalyzeCmd struct {
Path string `args:"--path"`
}
type Arguments struct {
Benchmark *BenchmarkCmd `arg:"subcommand:benchmark"`
Analyze *AnalyzeCmd `arg:"subcommand:analyze"`
}
func main() {
args := &Arguments{}
arg.MustParse(args)
err := run(*args)
if err != nil {
fmt.Println("unexpected error:", err)
os.Exit(1)
}
}
func run(args Arguments) error {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM)
defer cancel()
switch {
case args.Benchmark != nil:
return benchmark(ctx, *args.Benchmark)
case args.Analyze != nil:
return analyze(ctx, *args.Analyze)
default:
return fmt.Errorf("unknown command")
}
}
type Result struct {
Name string
Benchmarks []Benchmark
}
type Benchmark struct {
Image string
Measurements []Measurement
}
type Measurement struct {
Start time.Time
Stop time.Time
Duration time.Duration
}
func benchmark(ctx context.Context, args BenchmarkCmd) error {
cfg, err := clientcmd.BuildConfigFromFlags("", args.KubeconfigPath)
if err != nil {
return err
}
cs, err := kubernetes.NewForConfig(cfg)
if err != nil {
return err
}
dc, err := dynamic.NewForConfig(cfg)
if err != nil {
return err
}
ts := time.Now().Unix()
runName := fmt.Sprintf("spegel-benchmark-%d", ts)
_, err = cs.CoreV1().Namespaces().Get(ctx, args.Namespace, metav1.GetOptions{})
if err != nil && !errors.IsNotFound(err) {
return err
}
if errors.IsNotFound(err) {
ns := corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: args.Namespace,
},
}
_, err := cs.CoreV1().Namespaces().Create(ctx, &ns, metav1.CreateOptions{})
if err != nil {
return err
}
}
err = clearImages(ctx, cs, dc, args.Namespace, args.Images)
if err != nil {
return err
}
defer func() {
cs.AppsV1().DaemonSets(args.Namespace).Delete(ctx, runName, metav1.DeleteOptions{})
}()
result := Result{
Name: args.Name,
}
for _, image := range args.Images {
bench, err := measureImagePull(ctx, cs, dc, args.Namespace, runName, image)
if err != nil {
return err
}
result.Benchmarks = append(result.Benchmarks, bench)
}
err = clearImages(ctx, cs, dc, args.Namespace, args.Images)
if err != nil {
return err
}
fileName := fmt.Sprintf("%s.json", args.Name)
file, err := os.Create(path.Join(args.ResultDir, fileName))
if err != nil {
return err
}
defer file.Close()
b, err := json.MarshalIndent(result, "", " ")
if err != nil {
return err
}
_, err = file.Write(b)
if err != nil {
return err
}
return nil
}
func clearImages(ctx context.Context, cs kubernetes.Interface, dc dynamic.Interface, namespace string, images []string) error {
remove := fmt.Sprintf("crictl rmi %s || true", strings.Join(images, " "))
commands := []string{"/bin/sh", "-c", fmt.Sprintf("chroot /host /bin/bash -c '%s'; sleep infinity;", remove)}
ds := &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Name: "spegel-clear-image",
},
Spec: appsv1.DaemonSetSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "spegel-clear-image"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "spegel-clear-image",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "clear",
Image: "docker.io/library/alpine:3.18.4@sha256:48d9183eb12a05c99bcc0bf44a003607b8e941e1d4f41f9ad12bdcc4b5672f86",
ImagePullPolicy: "IfNotPresent",
Command: commands,
Stdin: true,
VolumeMounts: []corev1.VolumeMount{
{
Name: "host-root",
MountPath: "/host",
},
},
},
},
Volumes: []corev1.Volume{
{
Name: "host-root",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/",
},
},
},
},
},
},
},
}
_, err := cs.AppsV1().DaemonSets(namespace).Create(ctx, ds, metav1.CreateOptions{})
if err != nil && !errors.IsNotFound(err) {
return err
}
defer func() {
cs.AppsV1().DaemonSets(namespace).Delete(ctx, ds.ObjectMeta.Name, metav1.DeleteOptions{})
}()
err = wait.PollUntilContextTimeout(ctx, 1*time.Second, 10*time.Minute, true, func(ctx context.Context) (done bool, err error) {
gvr := schema.GroupVersionResource{
Group: "apps",
Version: "v1",
Resource: "daemonsets",
}
u, err := dc.Resource(gvr).Namespace(namespace).Get(ctx, ds.ObjectMeta.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
res, err := status.Compute(u)
if err != nil {
return false, err
}
if res.Status != status.CurrentStatus {
return false, nil
}
return true, nil
})
if err != nil {
return err
}
return nil
}
func measureImagePull(ctx context.Context, cs kubernetes.Interface, dc dynamic.Interface, namespace, name, image string) (Benchmark, error) {
ds, err := cs.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil && !errors.IsNotFound(err) {
return Benchmark{}, err
}
if errors.IsNotFound(err) {
ds := &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: appsv1.DaemonSetSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": name},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": name,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "benchmark",
Image: image,
ImagePullPolicy: "IfNotPresent",
// Keep container running
Stdin: true,
},
},
},
},
},
}
ds, err := cs.AppsV1().DaemonSets(namespace).Create(ctx, ds, metav1.CreateOptions{})
if err != nil {
return Benchmark{}, err
}
} else {
ds.Spec.Template.Spec.Containers[0].Image = image
_, err := cs.AppsV1().DaemonSets(namespace).Update(ctx, ds, metav1.UpdateOptions{})
if err != nil {
return Benchmark{}, err
}
}
err = wait.PollUntilContextTimeout(ctx, 1*time.Second, 30*time.Minute, true, func(ctx context.Context) (done bool, err error) {
gvr := schema.GroupVersionResource{
Group: "apps",
Version: "v1",
Resource: "daemonsets",
}
u, err := dc.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return false, err
}
res, err := status.Compute(u)
if err != nil {
return false, err
}
if res.Status != status.CurrentStatus {
return false, nil
}
return true, nil
})
if err != nil {
return Benchmark{}, err
}
podList, err := cs.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{LabelSelector: fmt.Sprintf("app=%s", name)})
if err != nil {
return Benchmark{}, err
}
if len(podList.Items) == 0 {
return Benchmark{}, fmt.Errorf("received empty benchmark pod list")
}
bench := Benchmark{
Image: image,
}
for _, pod := range podList.Items {
eventList, _ := cs.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.name=%s", pod.Name), TypeMeta: metav1.TypeMeta{Kind: "Pod"}})
if err != nil {
return Benchmark{}, err
}
pullingEvent, err := getEvent(eventList.Items, "Pulling")
if err != nil {
return Benchmark{}, err
}
pulledEvent, err := getEvent(eventList.Items, "Pulled")
if err != nil {
return Benchmark{}, err
}
d, err := parsePullMessage(pulledEvent.Message)
if err != nil {
return Benchmark{}, err
}
bench.Measurements = append(bench.Measurements, Measurement{Start: pullingEvent.FirstTimestamp.Time, Stop: pullingEvent.FirstTimestamp.Time.Add(d), Duration: d})
}
return bench, nil
}
func getEvent(events []corev1.Event, reason string) (corev1.Event, error) {
for _, event := range events {
if event.Reason != reason {
continue
}
return event, nil
}
return corev1.Event{}, fmt.Errorf("could not find event with reason %s", reason)
}
func parsePullMessage(msg string) (time.Duration, error) {
r, err := regexp.Compile(`\((.*) including waiting\)`)
if err != nil {
return 0, err
}
match := r.FindStringSubmatch(msg)
if len(match) < 2 {
return 0, fmt.Errorf("could not find image pull duration")
}
s := match[1]
d, err := time.ParseDuration(s)
if err != nil {
return 0, err
}
return d, nil
}
func analyze(ctx context.Context, args AnalyzeCmd) error {
b, err := os.ReadFile(args.Path)
if err != nil {
return err
}
result := Result{}
err = json.Unmarshal(b, &result)
if err != nil {
return err
}
ext := path.Ext(args.Path)
outPath := strings.TrimSuffix(args.Path, ext)
outPath = fmt.Sprintf("%s.png", outPath)
err = createPlot(result, outPath)
if err != nil {
return err
}
return nil
}
func createPlot(result Result, path string) error {
plots := []*plot.Plot{}
for _, bench := range result.Benchmarks {
p := plot.New()
p.Title.Text = bench.Image
p.Title.Padding = vg.Points(10)
p.Y.Label.Text = "Pod Number"
p.X.Label.Text = "Time [ms]"
slices.SortFunc(bench.Measurements, func(a, b Measurement) int {
if a.Start == b.Start {
return a.Stop.Compare(b.Stop)
}
return a.Start.Compare(b.Start)
})
zeroTime := bench.Measurements[0].Start
max := int64(0)
min := int64(0)
for i, result := range bench.Measurements {
if i == 0 || result.Duration.Milliseconds() < min {
min = result.Duration.Milliseconds()
}
if i == 0 || result.Duration.Milliseconds() > max {
max = result.Duration.Milliseconds()
}
start := result.Start.Sub(zeroTime)
stop := start + result.Duration
b, err := plotter.NewBoxPlot(4, float64(len(bench.Measurements)-i-1), plotter.Values{float64(start.Milliseconds()), float64(stop.Milliseconds())})
if err != nil {
return err
}
b.Horizontal = true
b.FillColor = color.Black
p.Add(b)
}
plots = append(plots, p)
}
img := vgimg.New(vg.Points(700), vg.Points(300))
dc := draw.New(img)
t := draw.Tiles{
Rows: 1,
Cols: len(plots),
PadX: vg.Millimeter,
PadY: vg.Millimeter,
PadTop: vg.Points(10),
PadBottom: vg.Points(10),
PadLeft: vg.Points(10),
PadRight: vg.Points(10),
}
canv := plot.Align([][]*plot.Plot{plots}, t, dc)
for i, plot := range plots {
plot.Draw(canv[0][i])
}
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
png := vgimg.PngCanvas{Canvas: img}
if _, err := png.WriteTo(file); err != nil {
return err
}
return nil
}

View File

@ -1,27 +0,0 @@
package main
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestParsePullMessage(t *testing.T) {
s := "Successfully pulled image \"docker.io/library/nginx:mainline-alpine\" in 873.420598ms (873.428863ms including waiting)"
d, err := parsePullMessage(s)
require.NoError(t, err)
require.Equal(t, 873428863*time.Nanosecond, d)
}
func TestCreatePlot(t *testing.T) {
results := []dsResult{}
for i := 1; i <= 10; i++ {
d, err := time.ParseDuration(fmt.Sprintf("%ds", i))
require.NoError(t, err)
results = append(results, dsResult{start: time.Now().Add(d), duration: d})
}
err := createPlot(results)
require.NoError(t, err)
}

View File

@ -1,64 +0,0 @@
module github.com/spegel-org/spegel/test/benchmark
go 1.21.3
require (
github.com/alexflint/go-arg v1.4.3
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
gonum.org/v1/plot v0.14.0
k8s.io/api v0.28.3
k8s.io/apimachinery v0.28.3
k8s.io/client-go v0.28.3
sigs.k8s.io/cli-utils v0.35.0
)
require (
git.sr.ht/~sbinet/gg v0.5.0 // indirect
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect
github.com/alexflint/go-scalar v1.1.0 // indirect
github.com/campoy/embedmd v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/go-fonts/liberation v0.3.1 // indirect
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-pdf/fpdf v0.8.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/image v0.11.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

View File

@ -1,223 +0,0 @@
git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo=
git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE=
git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8=
git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo=
github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA=
github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
github.com/go-fonts/latin-modern v0.3.1 h1:/cT8A7uavYKvglYXvrdDw4oS5ZLkcOU22fa2HJ1/JVM=
github.com/go-fonts/latin-modern v0.3.1/go.mod h1:ysEQXnuT/sCDOAONxC7ImeEDVINbltClhasMAqEtRK0=
github.com/go-fonts/liberation v0.3.1 h1:9RPT2NhUpxQ7ukUvz3jeUckmN42T9D9TpjtQcqK/ceM=
github.com/go-fonts/liberation v0.3.1/go.mod h1:jdJ+cqF+F4SUL2V+qxBth8fvBpBDS7yloUL5Fi8GTGY=
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs=
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9/go.mod h1:gWuR/CrFDDeVRFQwHPvsv9soJVB/iqymhuZQuJ3a9OM=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-pdf/fpdf v0.8.0 h1:IJKpdaagnWUeSkUFUjTcSzTppFxmv8ucGQyNPQWxYOQ=
github.com/go-pdf/fpdf v0.8.0/go.mod h1:gfqhcNwXrsd3XYKte9a7vM3smvU/jB4ZRDrmWSxpfdc=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE=
github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0=
gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU=
gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE=
gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM=
k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc=
k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4=
k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo=
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/cli-utils v0.35.0 h1:dfSJaF1W0frW74PtjwiyoB4cwdRygbHnC7qe7HF0g/Y=
sigs.k8s.io/cli-utils v0.35.0/go.mod h1:ITitykCJxP1vaj1Cew/FZEaVJ2YsTN9Q71m02jebkoE=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

Some files were not shown because too many files have changed in this diff Show More