Compare commits

...

197 Commits

Author SHA1 Message Date
Brian Goff
dfe657cfa5 Add trailing newline to stats response
This was just an annoyance when working with the API from a terminal.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2025-06-11 23:58:22 +01:00
Brian Goff
c01fff766d Update mock CLI to use bootstrapper
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2025-06-11 23:30:57 +01:00
Brian Goff
e72a73af61 Add nodeutil opt to bootstrapping from rest.Config
This uses a rest.Config to bootstrap TLS for the http server, webhook
auth, and the client.

This can be expanded later to do other kinds of TLS bootstrapping. For
now this seems to get the job done in terms of what VK expects for
permissions on the cluster.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2025-06-11 23:29:59 +01:00
dependabot[bot]
84502b708d build(deps): bump go.opentelemetry.io/otel/sdk from 1.33.0 to 1.36.0
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.33.0 to 1.36.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.33.0...v1.36.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 23:04:32 +01:00
dependabot[bot]
adde4a85df build(deps): bump go.opentelemetry.io/otel from 1.33.0 to 1.36.0
Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.33.0 to 1.36.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.33.0...v1.36.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel
  dependency-version: 1.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 22:46:33 +01:00
Enrico Candino
4ce429e6dd added envtest 2025-06-11 22:08:28 +01:00
dependabot[bot]
c9f64e8728 build(deps): bump github.com/prometheus/common from 0.61.0 to 0.63.0
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.61.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.61.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-05-30 11:38:08 +01:00
dependabot[bot]
65f6275bdf build(deps): bump golang.org/x/net from 0.34.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.34.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.34.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-05-29 19:56:46 +01:00
galal-hussein
0a453f92cf Check for EphemeralContainers changes in pod compare
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-05-29 19:31:22 +01:00
Pires
dd685be418 ci: revisit Go linter 2025-05-29 19:17:47 +01:00
Ville Vesilehto
ca78265381 fix: enable lll linter and format long function signatures
Previously lll linter was configured to .golangci.yml but with
a typo in the directive ('linter-settings', not 'linters-settings').

Enable line length linter and fix long lines by breaking function
signatures across multiple lines in exec.go, attach.go, and resource.go.

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
2025-05-29 17:44:47 +01:00
tskillian
5c534ffcd6 unit test 2025-01-17 20:13:09 +00:00
tskillian
47e15ff7ee Allow providers to skip downward API resolving 2025-01-17 20:13:09 +00:00
dependabot[bot]
d032e56103 build(deps): bump google.golang.org/protobuf from 1.35.2 to 1.36.2
Bumps google.golang.org/protobuf from 1.35.2 to 1.36.2.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-17 16:41:23 +00:00
dependabot[bot]
aff0d1483c build(deps): bump golang.org/x/time from 0.5.0 to 0.9.0
Bumps [golang.org/x/time](https://github.com/golang/time) from 0.5.0 to 0.9.0.
- [Commits](https://github.com/golang/time/compare/v0.5.0...v0.9.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>
2025-01-15 11:23:02 +00:00
Senyo Simpson
b8cfb1bf38 add license, comments and support for creating a logger from slog 2025-01-08 13:37:41 +00:00
Senyo Simpson
2ea2063625 add support for slog logger 2025-01-08 13:37:41 +00:00
dependabot[bot]
038f20adc0 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.16.0 to 1.20.5.
- [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.16.0...v1.20.5)

---
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-01-08 13:25:19 +00:00
dependabot[bot]
992c65f9be build(deps): bump go.opentelemetry.io/otel/sdk from 1.28.0 to 1.33.0
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.28.0 to 1.33.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.28.0...v1.33.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-08 11:50:28 +00:00
Pires
0d14dffb16 deps: fix potential high CVE-2024-45338 2025-01-08 11:31:11 +00:00
Pires
d29a09c8ba deps: fix potential critical CVE-2024-45337 2025-01-08 11:31:11 +00:00
dependabot[bot]
d2837b1657 build(deps): bump golang.org/x/crypto from 0.21.0 to 0.31.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.21.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.21.0...v0.31.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-08 11:10:04 +00:00
Pires
8ff762fb7b deps: adopt upstream Kubelet stats API 2025-01-08 10:59:24 +00:00
Pires
164bd5b3e4 deps: removed unnecessary locked deps 2025-01-08 10:59:24 +00:00
Pires
b4c0110e35 deps: bump controller-runtime to v0.19.4 2025-01-08 10:59:24 +00:00
tskillian
fafac75ae7 k8s: adopt v1.31.4
And bump a bunch of dependencies.
2025-01-08 10:59:24 +00:00
Pires
78b11dfcbb build: Dockerfile env vars have a new format 2025-01-08 10:59:24 +00:00
Pires
f3c3f0af9a ci: run it all on Ubuntu 22.04 LTS 2025-01-08 10:59:24 +00:00
Pires
f37cdd9215 e2e: adopt Kubernetes 1.31.x 2025-01-08 10:59:24 +00:00
Pires
3611df8e83 build: adopt Skaffold v2.13.x 2025-01-08 10:59:24 +00:00
Pires
4942ea59a7 build: adopt Go 1.23 and bump linter 2025-01-08 10:59:24 +00:00
Jared Allard
1f5e83b1d6 docs(README): typo in PodLifecycleHandler (#1237) 2024-08-26 14:07:55 -07:00
Diego Ciangottini
921890c7cb Amend interLink gh organization on website (#1218)
Co-authored-by: Heba <31887807+helayoty@users.noreply.github.com>
2024-08-26 14:04:15 -07:00
dependabot[bot]
0ab48b08ae build(deps): bump golang.org/x/sys from 0.18.0 to 0.24.0 (#1238)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 13:45:06 -07:00
dependabot[bot]
7676cc320c build(deps): bump github.com/prometheus/client_model from 0.4.0 to 0.6.1 (#1222)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-15 17:03:13 -07:00
dependabot[bot]
8f2e4f2064 build(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 (#1221)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-15 17:02:55 -07:00
dependabot[bot]
cd93c79e07 build(deps): bump github.com/gorilla/mux from 1.8.0 to 1.8.1 (#1219)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-15 17:02:30 -07:00
dependabot[bot]
5b9c1ce860 Bump golang.org/x/time from 0.3.0 to 0.5.0 (#1197)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Heba <31887807+helayoty@users.noreply.github.com>
2024-05-10 21:52:35 +00:00
dependabot[bot]
6aeccbef59 Bump github/codeql-action from 2 to 3
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-10 22:47:49 +01:00
dependabot[bot]
76547669a8 Bump golang.org/x/sync from 0.5.0 to 0.7.0 (#1209)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Heba <31887807+helayoty@users.noreply.github.com>
2024-05-10 14:40:14 -07:00
Diego Ciangottini
762d5fb2a5 interLink provider documentation (#1214)
Signed-off-by: Diego Ciangottini <diego.ciangottini@pg.infn.it>
Co-authored-by: Heba <31887807+helayoty@users.noreply.github.com>
2024-05-10 21:29:29 +00:00
dependabot[bot]
6ae69e26f5 Bump golang.org/x/net from 0.19.0 to 0.23.0 (#1211)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Heba <31887807+helayoty@users.noreply.github.com>
2024-05-10 21:20:37 +00:00
dependabot[bot]
663557ce2b Bump google.golang.org/protobuf from 1.31.0 to 1.34.1 (#1215)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Heba <31887807+helayoty@users.noreply.github.com>
2024-05-10 14:14:58 -07:00
dependabot[bot]
47c22a831d Bump golangci/golangci-lint-action from 4 to 6 (#1217)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Heba <31887807+helayoty@users.noreply.github.com>
2024-05-10 21:03:00 +00:00
dependabot[bot]
b6e6295c17 Bump k8s.io/klog/v2 from 2.110.1 to 2.120.1 (#1196)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Heba <31887807+helayoty@users.noreply.github.com>
2024-05-09 23:53:58 +00:00
dependabot[bot]
a3d681ee27 Bump github.com/spf13/cobra from 1.7.0 to 1.8.0 (#1194)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Heba <31887807+helayoty@users.noreply.github.com>
2024-05-09 16:50:03 -07:00
Renzheng Wang
154fe08556 ci: fix e2e test (#1216) 2024-05-09 16:39:32 -07:00
Jose Fernandez
238aebad73 Expose queue unprocessed and items being processed len 2024-04-01 11:38:15 +01:00
dependabot[bot]
2ee2d9f63b Bump golangci/golangci-lint-action from 3 to 4
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)

---
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-03-30 21:05:08 +00:00
dependabot[bot]
d41d02609f Bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc from 0.42.0 to 0.46.0 (#1176)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-19 11:18:35 -08:00
dependabot[bot]
1236adf762 Bump go.opentelemetry.io/otel/sdk from 1.19.0 to 1.22.0 (#1190)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-19 11:12:21 -08:00
dependabot[bot]
617c3f0615 Bump k8s.io/apiserver from 0.27.3 to 0.29.1 (#1189)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-19 10:50:54 -08:00
Francesco Torta
a9ce60e19d feat: support SPDY subprotocol version v5 (K8s API >= v1.29.0) (#1185) 2024-01-19 10:31:38 -08:00
dependabot[bot]
d6a4e8ee09 Bump golang.org/x/crypto from 0.1.0 to 0.17.0 (#1188)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-18 16:36:27 -08:00
dependabot[bot]
1197d5907b Bump golang.org/x/net from 0.10.0 to 0.17.0 (#1167)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-18 16:26:33 -08:00
guangwu
11c9c6cda0 chore: pkg import only once (#1175)
Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>
Co-authored-by: Heba <31887807+helayoty@users.noreply.github.com>
2024-01-18 16:20:04 -08:00
dependabot[bot]
0b9dcbb100 Bump google.golang.org/grpc from 1.55.0 to 1.56.3 (#1173)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-18 16:14:38 -08:00
dependabot[bot]
21a7a61cd2 Bump actions/setup-go from 4 to 5 (#1183)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-18 15:56:19 -08:00
HeGaoYuan
ce8a0ee8bd typo: update comment (#1164) 2023-09-25 06:57:39 +00:00
dependabot[bot]
440bcf0535 Bump golang.org/x/sys from 0.9.0 to 0.12.0 (#1156)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-14 19:07:28 +00:00
zhenqxuMSFT
dd31d67a53 Add error handler to recreate virtual node when it's deleted (#1143) 2023-09-14 11:58:27 -07:00
dependabot[bot]
c5478eabb2 Bump actions/checkout from 3 to 4 (#1155)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-14 18:43:56 +00:00
Jose Fernandez
394128a0f2 feat: Add methods to get length of PodController queues (#1145) 2023-09-14 18:32:19 +00:00
Alejandro Cuadron Lafuente
72045b221b Fixed typo (#1136) 2023-08-21 15:03:53 -07:00
dependabot[bot]
34c2ecc2ef Bump github.com/prometheus/client_golang from 1.15.1 to 1.16.0 (#1131)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-27 22:22:12 +00:00
dependabot[bot]
da4b353793 Bump google.golang.org/protobuf from 1.30.0 to 1.31.0 (#1130) 2023-06-27 21:10:56 +00:00
dependabot[bot]
c590daf8f0 Bump k8s.io/klog/v2 from 2.90.1 to 2.100.1 (#1132) 2023-06-27 14:04:14 -07:00
dependabot[bot]
7699abc706 Bump github.com/bombsimon/logrusr/v3 from 3.0.0 to 3.1.0 (#1133)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-27 13:34:16 -07:00
dependabot[bot]
acd162bf4d Bump github.com/stretchr/testify from 1.8.3 to 1.8.4 (#1134)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-27 13:20:09 -07:00
dependabot[bot]
ff0d16c4c7 Bump k8s.io/apiserver from 0.27.2 to 0.27.3 (#1124)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-21 00:02:42 +00:00
dependabot[bot]
84af992d29 Bump github.com/spf13/cobra from 1.6.0 to 1.7.0 (#1126)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-20 16:57:09 -07:00
dependabot[bot]
0f2ca47f3d Bump golang.org/x/sync from 0.1.0 to 0.3.0 (#1125)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-20 23:49:33 +00:00
dependabot[bot]
816fde5dcb Bump github.com/sirupsen/logrus from 1.9.0 to 1.9.3 (#1127)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-20 16:42:42 -07:00
dependabot[bot]
c461c8d9d4 Bump github.com/prometheus/common from 0.42.0 to 0.44.0 (#1128)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-20 12:38:42 -07:00
Salvatore Cirone
59fd7fddb6 add portforwarding support to node/api (#1102)
Co-authored-by: Pablo Borrelli <pablo.borrelli0@gmail.com>
Co-authored-by: windyear <1280027646@qq.com>
2023-06-15 18:45:10 -07:00
dependabot[bot]
8205ee2889 Bump sigs.k8s.io/controller-runtime from 0.13.0 to 0.15.0 (#1119)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-16 01:38:06 +00:00
dependabot[bot]
f21325ab41 Bump k8s.io/client-go from 0.27.0 to 0.27.3 (#1123)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-15 18:31:32 -07:00
Heba Elayoty
077ee93fa2 fix: Fix opentelemetry dependencies issues (#1122)
Signed-off-by: Heba Elayoty <hebaelayoty@gmail.com>
2023-06-15 18:23:16 -07:00
David
ad4739b7e4 Update providers.yaml - StackPath (#1121) 2023-06-08 18:19:09 +00:00
shayrybak
9a9e2ed47a Add Stackpath provider (#1112) 2023-06-08 10:17:35 -07:00
Kevin Hannon
52eacaa577 e2e: support kind clusters (#1103) 2023-04-15 12:11:42 +00:00
pigletfly
4a14603c56 bump golang version to 1.19
Signed-off-by: pigletfly <wangbing.adam@gmail.com>
2023-04-14 12:47:29 +01:00
fnuarnav
2c155accb7 Prometheus metrics are encoded as text, not JSON (#1101)
Co-authored-by: Sanchit Mehta <sanchit.mehta602@gmail.com>
2023-04-06 08:03:43 +01:00
Salvatore Cirone
9c32bfb0ae Add support for Attach API functionality (#1090)
Co-authored-by: Pablo Borrelli <pablo.borrelli0@gmail.com>
2023-03-31 08:51:50 -07:00
Jackie Lan
b7030b9dc5 Support advanced capacity and providerID settings in mock provider 2023-03-28 13:22:44 +01:00
fnuarnav
a457d445a3 feat: Implement new metrics endpoint for k8s 1.24+ (#1082) 2023-03-28 13:01:37 +01:00
fnuarnav
b70ee9b6dd New metrics API proposal (#1075) 2023-03-28 12:39:42 +01:00
dependabot[bot]
8bf7691f59 Bump github.com/prometheus/client_golang from 1.13.0 to 1.14.0 (#1095)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-27 11:10:35 -07:00
dependabot[bot]
d87cc6ee1a Bump k8s.io/klog/v2 from 2.80.1 to 2.90.1 (#1091)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Heba Elayoty <31887807+helayoty@users.noreply.github.com>
2023-03-20 23:45:06 +00:00
dependabot[bot]
2b6bd337cc Bump actions/setup-go from 3 to 4 (#1093)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-20 16:40:03 -07:00
Heba Elayoty
a2070739bb fix: Fix missing Backoff property for WebHookAuth (#1089) 2023-03-16 02:24:23 +00:00
dependabot[bot]
a90f71b9a4 Bump golang.org/x/time from 0.0.0-20220609170525-579cf78fd858 to 0.3.0
Bumps [golang.org/x/time](https://github.com/golang/time) from 0.0.0-20220609170525-579cf78fd858 to 0.3.0.
- [Release notes](https://github.com/golang/time/releases)
- [Commits](https://github.com/golang/time/commits/v0.3.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>
2023-03-14 10:52:11 +00:00
Pires
dcbb102f53 cmd: fix nil pointer during node setup 2023-03-14 10:44:31 +00:00
dependabot[bot]
90f81e9cc7 Bump k8s.io/api from 0.25.0 to 0.26.2 (#1078)
Co-authored-by: Pires <pjpires@gmail.com>
2023-03-13 11:10:19 +00:00
Pires
eb5d959215 replace deprecated pointer funcs 2023-03-13 10:56:38 +00:00
dependabot[bot]
109b1eed8b Bump k8s.io/apimachinery from 0.25.0 to 0.26.2
Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.25.0 to 0.26.2.
- [Release notes](https://github.com/kubernetes/apimachinery/releases)
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.25.0...v0.26.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-13 10:56:38 +00:00
Xiang Li
5e4340a4a4 Add vendor directory to gitignore (#957)
Co-authored-by: Heba Elayoty <31887807+helayoty@users.noreply.github.com>
2023-02-28 23:06:56 -08:00
Heba Elayoty
b8f8449177 Update google calendar and meeting time 2023-02-28 22:31:48 -08:00
dependabot[bot]
d23c36eec6 Bump golang.org/x/net from 0.0.0-20220722155237-a158d28d115b to 0.7.0 (#1077)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-28 21:58:33 -08:00
Brian Goff
7bcacb1cab Merge pull request #1060 from edigaryev/patch-1
README.md: add go.dev badge
2022-12-15 15:15:03 -08:00
Nikolay Edigaryev
a610358f56 README.md: add go.dev badge 2022-12-09 22:34:17 +04:00
dependabot[bot]
704b01eac6 Bump sigs.k8s.io/controller-runtime from 0.7.1 to 0.13.0 (#1034)
Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.7.1 to 0.13.0.
- [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases)
- [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/master/RELEASE.md)
- [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.7.1...v0.13.0)

---
updated-dependencies:
- dependency-name: sigs.k8s.io/controller-runtime
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-14 13:56:42 -07:00
Heba Elayoty
94999fc0b6 Merge pull request #1035 from virtual-kubelet/dependabot/go_modules/k8s.io/klog/v2-2.80.1
Bump k8s.io/klog/v2 from 2.2.0 to 2.80.1
2022-10-14 13:32:39 -07:00
dependabot[bot]
9d8005e4b8 Bump k8s.io/klog/v2 from 2.2.0 to 2.80.1
Bumps [k8s.io/klog/v2](https://github.com/kubernetes/klog) from 2.2.0 to 2.80.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.2.0...v2.80.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>
2022-10-10 17:37:07 +00:00
Brian Goff
a486eaffd2 Merge pull request #1046 from cpuguy83/codeql_perms
codeql: Add explicit permissions
2022-10-10 10:36:08 -07:00
Brian Goff
d66366ba96 codeql: Add explicit permissions
Codeql requires write access to security-events, but our default action
token (rightly) only has read permissions.
This adds the explicit request for write access.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-10-10 17:29:32 +00:00
Brian Goff
bb4e20435d Merge pull request #1042 from cpuguy83/bumps
Bump packages
2022-10-08 10:51:33 -07:00
Brian Goff
6feafcf018 Remove klogv2 alias
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-10-07 23:21:47 +00:00
Brian Goff
5db1443e33 Fix apparent bad copy/pasta in test causing panic
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-10-07 23:21:47 +00:00
Brian Goff
2c4442b17f Fix linting issues and update make lint target.
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-10-07 23:21:47 +00:00
Brian Goff
70848cfdae Bump k8s deps to v0.24
This requires dropping otel down to v0.20 because the apiserver package
is importing it and some packages moved around with otel v1.
Even k8s v0.25 still uses this old version of otel, so we are stuck for
a bit (v0.26 will, as of now, use a newer otel version).

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-10-07 23:21:47 +00:00
Brian Goff
c668ae6ab6 Bump problematic deps
Changes in klog and logr have made automatic bumps from dependabot
problematic.
We also shouldn't need klogv1 so removed that.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-10-07 23:21:47 +00:00
Brian Goff
f7f8b45117 Bump go version to 1.18 in dockerfile
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-10-07 23:21:47 +00:00
Brian Goff
5001135763 Merge pull request #1043 from LuBingtan/add-default-client
Add default client in mock provider
2022-09-30 09:08:50 -07:00
lubingtan
67be3c681d Add default client
Signed-off-by: lubingtan <bingtlu@ebay.com>
2022-09-30 09:47:22 +08:00
Brian Goff
fca742986c Merge pull request #1036 from virtual-kubelet/dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2 to 3
2022-09-12 11:07:19 -07:00
dependabot[bot]
db7f53c1ca Bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-12 17:33:34 +00:00
Brian Goff
c63b8f0dec Merge pull request #994 from pigletfly/bump-go
bump golang to 1.17
2022-09-12 09:38:09 -07:00
pigletfly
83fbc0c687 bump golang to 1.17
Signed-off-by: pigletfly <wangbing.adam@gmail.com>
2022-09-07 09:34:00 +08:00
Brian Goff
d2523fe808 Merge pull request #1031 from cpuguy83/fix_envtest_name
Fix typo in job name
2022-08-31 14:03:00 -07:00
Brian Goff
7ee822ec6d Fix typo in job name 2022-08-31 21:02:12 +00:00
Brian Goff
0b70fb1958 Merge pull request #1025 from virtual-kubelet/dependabot/go_modules/contrib.go.opencensus.io/exporter/jaeger-0.2.1
Bump contrib.go.opencensus.io/exporter/jaeger from 0.1.0 to 0.2.1
2022-08-31 13:15:00 -07:00
dependabot[bot]
a25c1def45 Bump contrib.go.opencensus.io/exporter/jaeger from 0.1.0 to 0.2.1
Bumps [contrib.go.opencensus.io/exporter/jaeger](https://github.com/census-ecosystem/opencensus-go-exporter-jaeger) from 0.1.0 to 0.2.1.
- [Release notes](https://github.com/census-ecosystem/opencensus-go-exporter-jaeger/releases)
- [Commits](https://github.com/census-ecosystem/opencensus-go-exporter-jaeger/compare/v0.1.0...v0.2.1)

---
updated-dependencies:
- dependency-name: contrib.go.opencensus.io/exporter/jaeger
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-31 19:58:59 +00:00
Brian Goff
d682bb3894 Merge pull request #1028 from cpuguy83/codeql_nopr
Only run codeql on pushes to master, not pr's
2022-08-31 12:57:42 -07:00
Brian Goff
6198b02423 Only run codeql on pushes to master, not pr's
These are extremely slow and probably very expensive for someone.
We don't need these running on PR's which have constant pushes, rebases,
etc.

The activity on the repo is slow enough we can fix-up things after
codeql runs on master.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-08-31 19:56:04 +00:00
Brian Goff
9d94eea9e9 Merge pull request #1023 from virtual-kubelet/dependabot/github_actions/github/codeql-action-2
Bump github/codeql-action from 1 to 2
2022-08-31 12:48:19 -07:00
dependabot[bot]
de4fe42586 Bump github/codeql-action from 1 to 2
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v1...v2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-31 19:42:26 +00:00
Brian Goff
305e33bfbf Merge pull request #1022 from virtual-kubelet/dependabot/github_actions/actions/setup-go-3
Bump actions/setup-go from 2 to 3
2022-08-31 12:41:50 -07:00
dependabot[bot]
00d8340a64 Bump actions/setup-go from 2 to 3
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-31 19:36:55 +00:00
Brian Goff
5e5a842dbb Merge pull request #1021 from cpuguy83/dependabot_actions
dependabot: update github actions
2022-08-31 12:36:37 -07:00
Brian Goff
aa94284712 dependabot: update github actions
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-08-31 19:35:43 +00:00
Brian Goff
d87dd1c79f Merge pull request #1018 from virtual-kubelet/dependabot/go_modules/contrib.go.opencensus.io/exporter/ocagent-0.7.0
Bump contrib.go.opencensus.io/exporter/ocagent from 0.4.12 to 0.7.0
2022-08-31 12:31:16 -07:00
dependabot[bot]
ab3615b8d7 Bump contrib.go.opencensus.io/exporter/ocagent from 0.4.12 to 0.7.0
Bumps [contrib.go.opencensus.io/exporter/ocagent](https://github.com/census-ecosystem/opencensus-go-exporter-ocagent) from 0.4.12 to 0.7.0.
- [Release notes](https://github.com/census-ecosystem/opencensus-go-exporter-ocagent/releases)
- [Commits](https://github.com/census-ecosystem/opencensus-go-exporter-ocagent/compare/v0.4.12...v0.7.0)

---
updated-dependencies:
- dependency-name: contrib.go.opencensus.io/exporter/ocagent
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-31 19:26:33 +00:00
Brian Goff
22d2416dc4 Merge pull request #1020 from virtual-kubelet/dependabot/go_modules/github.com/prometheus/client_golang-1.13.0
Bump github.com/prometheus/client_golang from 1.7.1 to 1.13.0
2022-08-31 12:25:37 -07:00
dependabot[bot]
e1c6e80a7a Bump github.com/prometheus/client_golang from 1.7.1 to 1.13.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.7.1 to 1.13.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.7.1...v1.13.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>
2022-08-31 19:20:54 +00:00
Brian Goff
1ed3180ec2 Merge pull request #1019 from virtual-kubelet/dependabot/go_modules/github.com/spf13/cobra-1.5.0
Bump github.com/spf13/cobra from 1.0.0 to 1.5.0
2022-08-31 12:19:55 -07:00
dependabot[bot]
97452b493f Bump github.com/spf13/cobra from 1.0.0 to 1.5.0
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.0.0 to 1.5.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.0.0...v1.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-31 18:40:59 +00:00
Brian Goff
6363360781 Merge pull request #1004 from mxplusb/feature/dependabot
added basic dependabot integration.
2022-08-31 11:40:07 -07:00
Brian Goff
44d0df547d Merge branch 'master' into feature/dependabot 2022-08-31 11:30:14 -07:00
Brian Goff
10a7559b83 Merge pull request #1006 from yabuchan/opentelemetry
Add opentelemetry as tracing provider
2022-08-31 11:29:56 -07:00
Brian Goff
b98ba29b52 Merge branch 'master' into opentelemetry 2022-08-31 11:09:42 -07:00
Brian Goff
008fe17b91 Merge pull request #1015 from cpuguy83/gh_actions
Add github actions
2022-08-31 11:00:53 -07:00
Brian Goff
ec1fe2070a Merge pull request #1013 from solarkennedy/k8s_1.24_compat
Removed deprecated node Clustername
2022-08-30 22:37:43 -07:00
Brian Goff
48e29d75fc Remove circleci
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-08-31 00:58:56 +00:00
Brian Goff
f617ccebc5 Fixup some new lint issues
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-08-31 00:58:56 +00:00
Brian Goff
433e0bbd20 Add github actions
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-08-31 00:58:51 +00:00
Kyle Anderson
a8f253088c Removed deprecated node Clustername
With this one line, vk fails to build against k8s 1.24 libs.

The comment says:

    // Deprecated: ClusterName is a legacy field that was always cleared by
    // the system and never used; it will be removed completely in 1.25.

Seems to be removed in 1.24 though.
2022-08-25 19:39:11 -07:00
yabuchan
38e662129d remove warning from golinter 2022-07-08 10:23:17 +09:00
yabuchan
95bdbdec0d reflect review comments 2022-07-08 00:18:58 +09:00
yabuchan
1958686b4a remove unnecessary lines 2022-07-04 21:38:38 +09:00
yabuchan
36397f80c2 downgrade opentelemetry to avoid the library conflicts 2022-07-04 19:15:45 +09:00
yabuchan
801b44543c do not save attributes in logger, add fake logger in unit test 2022-07-04 13:52:51 +09:00
yabuchan
853f9ead1c add sampler in test 2022-07-04 00:22:57 +09:00
yabuchan
915445205f add logic for otel 2022-07-03 23:30:10 +09:00
Sienna Lloyd
2b7e4c9dc6 added basic dependabot integration.
Signed-off-by: Sienna Lloyd <sienna.lloyd@hey.com>
2022-06-23 11:30:16 -06:00
Brian Goff
410e05878a Merge pull request #985 from cbsfly/fix-klog
Change klog flag version to v2
2021-11-23 07:28:04 -08:00
chenbingshen.invoker
70c7745444 Change klog flag version to v2 2021-11-18 18:29:26 +08:00
Brian Goff
269ef14a7a Merge pull request #984 from pigletfly/fix-readme
Add scrape pod metrics
2021-11-16 13:54:57 -08:00
pigletfly
faaf14c68d Add scrape pod metrics 2021-11-12 10:56:46 +08:00
Brian Goff
7c9bd20eea Merge pull request #975 from cpuguy83/node_manager
Make ControllerManager more useful
2021-09-14 10:26:48 -07:00
Brian Goff
c9c0d99064 Rename NewNodeFromClient to just NewNode
Since we now store the client on the config, we don't need to use a
custom client.
2021-09-14 17:10:17 +00:00
Brian Goff
4974e062d0 Add webhook and anon auth support
Auth is not automatically enabled because this requires some
bootstrapping to work.
I'll leave this for some future work.
In the meantime people can use the current code similar to how they used
the node-cli code to inject their own auth.
2021-09-14 17:10:17 +00:00
Brian Goff
e1342777d6 Add API config to node set
This moves API handling into the node object so now everything can be
done in one place.

TLS is required.
In the current form, auth must be setup by the caller.
2021-09-14 17:10:17 +00:00
Brian Goff
597e7dc281 Make ControllerManager more useful
This changes `ControllerManager` to `Node`.

`Node` is created from a client where the VK lib is responsible for
creating all the things except the client (unless client is nil, then we
use the env client).

This should be a good replacement for node-cli.  It offers a simpler
API.  *It only works with leases enabled* since this seems always
desired, however an option could be added to disable if needed.

The intent of this is to provide a simpler way to get a vk node up and
running while also being extensible. We can slowly add options, but
they should be focussed on a use-case rather than trying to support
every possible scenario... in which case the user can just use the
controllers directly.
2021-09-14 17:10:14 +00:00
Brian Goff
a9a0ee50cf Remove create-after-delete node e2e tst
This test is only testing the sepcific implementation details of the
mock CLI provided in this repo. The behavior is not inherent in the vk
lib.
2021-09-14 16:57:43 +00:00
Brian Goff
5fe8a7d000 Merge pull request #979 from cpuguy83/fix_ping_panic
Return early on ping error
2021-09-03 12:02:55 -07:00
Brian Goff
22f329fcf0 Add extra logging for pod status update skip 2021-09-03 18:02:35 +00:00
Brian Goff
09ad3fe644 Return early on ping error
Found that this caused a panic after many many test runs.
It seems like we should have returned early since the pingResult is nil.
We don't want to update a lease when ping fails.
2021-08-24 18:49:42 +00:00
Brian Goff
68347d4ed1 Merge pull request #967 from cpuguy83/controller_manager2
Move some boiler plate startup logic to nodeutil
2021-06-01 12:05:59 -07:00
Brian Goff
92f8661031 Merge pull request #973 from cpuguy83/ci_store_test_results
Output test results in junit and export to circle
2021-06-01 11:32:21 -07:00
Brian Goff
f63c23108f Move some boiler plate startup logic to nodeutil
This makes a controller that handles the startup for the node and pod
controller.
Later if we add an "api controller" it can also be added here.

This is just part of reducing some of the boiler plate code so it is
easier to get off of node-cli.
2021-05-25 17:54:53 +00:00
Brian Goff
db5bf2b0d3 Output test results in junit and export to circle 2021-05-20 17:20:27 +00:00
Brian Goff
fbf6a1957f Merge pull request #970 from palexster/apa/add_liqo
Adding Liqo to README.md
2021-05-19 14:32:20 -07:00
Brian Goff
e6fc00e8dd Merge pull request #971 from champly/fix-jaeger-deprecated-config
fix jaeger deprecated config
2021-05-19 14:31:11 -07:00
champly
50f1346977 fix staticcheck 2021-05-19 09:17:06 +08:00
champly
66fc9d476f fix staticcheck 2021-05-19 09:13:28 +08:00
Brian Goff
0543245668 lifecycle test: timeout send goroutine on context
In error cases these goroutines never exit.
Trying to debug cases we end up with a bunch of these goroutines stuck
making it difficult to troubleshoot.

We could just make a buffered channel, however this will makes it less
clear, in cases of an error, what all is happening.
2021-05-18 23:06:55 +00:00
Brian Goff
d245d9b8cf Merge branch 'master' into apa/add_liqo 2021-05-18 13:36:53 -07:00
Brian Goff
4fe8496dd1 Fix TestMapReference needed an ordered mapping
In 405d5d63b1 we changed for an ordered
list to a map, however the test is order dependent go maps are
randomized.

Change the test to use a slice with an internal type (instead of pulling
back in k8s.io/kubernetes).

Without this change this test will fail occasionally and has no
guarentee for success because of the random order of maps.
2021-05-18 19:07:46 +00:00
Brian Goff
5cd25230c5 Merge pull request #972 from cpuguy83/remove_kk
Remove remaining deps on k8s.io/kubernetes
2021-05-18 09:22:30 -07:00
Brian Goff
04cdec767b Remove remaining deps on k8s.io/kubernetes
These are mostly helper code for setting up env vars.  There is a single
file copied verbatim, although much of this downward api stuff is a
copy.  We may want to pull this out and do a direct copy of the the code
so it is easier to update and work with upstream to have a shared
package that lives outside of k8s.io/kubernetes for downward api.
2021-05-17 21:42:49 +00:00
champly
822dc8bb4a Merge branch 'master' into fix-jaeger-deprecated-config 2021-05-16 20:12:38 +08:00
Brian Goff
40b4425804 Merge pull request #811 from TBBle/patch-1
Fix non-linked reference to Providers in Usage
2021-05-14 11:01:37 -07:00
Brian Goff
be0a062aec Merge pull request #969 from cpuguy83/copy_metrics
Copy stats types from upstream.
2021-05-13 16:26:53 -07:00
Paul "Hampy" Hampson
a2515d859a Fix non-linked reference to Providers in Usage
Fixes a typo ("provides") and also replaces "listed above" with a link to the list. Which had moved below, as it happens.
2021-05-14 02:45:33 +10:00
champly
0df7ac4e80 fix jaeger deprecated config 2021-05-12 10:36:58 +08:00
Alex Palesandro
96eae1906b Adding Liqo to README.md 2021-05-07 22:58:46 +02:00
Brian Goff
8437e237be Copy stats types from upstream.
This drops another dependency on k8s.io/kubernetes.
This does have the unfortunate side effect that implementers will now
get a compile error until they update their code to use the new type.

Just as a note:

The stats types have moved to k8s.io/kubelet, however the stats types
are only there as of v1.20.
Currently we support older versions than v1.20, and even our go.mod
imports from v1.19.

For now we copy the types in. Later we can remove the type defs and
change them to type aliases to the k8s.io/kubelet types (which prevents
another compile time issue).

Anything relying on type assertions to determine if something implements
this method will, unfortunately, be broken and it will be hard to notice
until runtime. We need to make sure to call this out in the release
notes.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2021-05-05 23:01:52 +00:00
Brian Goff
baa0e6e8fc Merge pull request #968 from cpuguy83/cleanup_some_kk
Don't import pod util package from k/k
2021-05-04 17:26:49 -07:00
Brian Goff
405d5d63b1 Don't import pod util package from k/k
These are all simple changes that will not change w/o breaking API
changes upstream anyway.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2021-05-04 23:55:30 +00:00
Brian Goff
e1486ade00 Merge pull request #966 from sargun/upgrade-k8s
Upgrade k8s to v19
2021-04-25 07:18:39 -07:00
Sargun Dhillon
4c223a8cd9 Upgrade to Kubernetes 19.10
Kubernetes 18.X is deprecated and no longer receiving updates.

Signed-off-by: Sargun Dhillon <sargun@sargun.me>
2021-04-23 00:47:51 -07:00
Brian Goff
bf3a764409 Merge pull request #962 from sargun/expose-custom-retry 2021-04-15 15:35:34 -07:00
Sargun Dhillon
b259cb0548 Add the ability to dictate custom retries
Our current retry policy is naive and only does 20 retries. It is
also based off of the rate limiter. If the user is somewhat aggressive in
rate limiting, but they have a temporary outage on API server, they
may want to continue to delay.

In facts, K8s has a built-in function to suggest delays:
https://pkg.go.dev/k8s.io/apimachinery/pkg/api/errors#SuggestsClientDelay

Signed-off-by: Sargun Dhillon <sargun@sargun.me>
2021-04-14 10:52:26 -07:00
Sargun Dhillon
e95023b76e Fix test
This starts watching for events prior to the start of the controller.
This smells like a bug in the fakeclient bits, but it seems to fix
the problem.

Signed-off-by: Sargun Dhillon <sargun@sargun.me>
2021-04-14 10:52:26 -07:00
Sargun Dhillon
5fd08d4619 Merge pull request #958 from sargun/fix-deletionQ
Remove errant double queue
2021-03-24 12:12:29 -07:00
Sargun Dhillon
c40a255eae Remove errant double queue
This seems to be a typo where we erroneously double-queue a deletion,
but one without the "key".
2021-03-24 10:21:27 -07:00
93 changed files with 6316 additions and 1881 deletions

View File

@@ -1,150 +0,0 @@
version: 2.0
jobs:
validate:
resource_class: xlarge
docker:
- image: circleci/golang:1.15
environment:
GO111MODULE: "on"
GOPROXY: https://proxy.golang.org
working_directory: /go/src/github.com/virtual-kubelet/virtual-kubelet
steps:
- checkout
- restore_cache:
keys:
- validate-{{ checksum "go.mod" }}-{{ checksum "go.sum" }}
- run:
name: go vet
command: V=1 CI=1 make vet
- run:
name: Lint
command: make lint
- run:
name: Dependencies
command: scripts/validate/gomod.sh
- save_cache:
key: validate-{{ checksum "go.mod" }}-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
test:
resource_class: xlarge
docker:
- image: circleci/golang:1.15
environment:
GO111MODULE: "on"
working_directory: /go/src/github.com/virtual-kubelet/virtual-kubelet
steps:
- checkout
- restore_cache:
keys:
- test-{{ checksum "go.mod" }}-{{ checksum "go.sum" }}
- run:
name: Build
command: V=1 make build
- run:
name: Tests
command: V=1 CI=1 make test envtest
- save_cache:
key: test-{{ checksum "go.mod" }}-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
e2e:
machine:
image: ubuntu-1604:202010-01
working_directory: /home/circleci/go/src/github.com/virtual-kubelet/virtual-kubelet
environment:
CHANGE_MINIKUBE_NONE_USER: true
GOPATH: /home/circleci/go
KUBECONFIG: /home/circleci/.kube/config
KUBERNETES_VERSION: v1.20.1
MINIKUBE_HOME: /home/circleci
MINIKUBE_VERSION: v1.16.0
MINIKUBE_WANTUPDATENOTIFICATION: false
MINIKUBE_WANTREPORTERRORPROMPT: false
SKAFFOLD_VERSION: v1.17.2
GO111MODULE: "on"
steps:
- checkout
- run:
name: Install kubectl
command: |
curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/${KUBERNETES_VERSION}/bin/linux/amd64/kubectl
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
mkdir -p ${HOME}/.kube
touch ${HOME}/.kube/config
- run:
name: Install Skaffold
command: |
curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/${SKAFFOLD_VERSION}/skaffold-linux-amd64
chmod +x skaffold
sudo mv skaffold /usr/local/bin/
- run:
name: Install Minikube dependencies
command: |
sudo apt-get update && sudo apt-get install -y apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
sudo apt-get update
sudo apt-get install -y kubelet # systemd unit is disabled
- run:
name: Install Minikube
command: |
curl -Lo minikube https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-amd64
chmod +x minikube
sudo mv minikube /usr/local/bin/
- run:
name: Start Minikube
command: |
sudo -E minikube start --vm-driver=none --cpus 2 --memory 2048 --kubernetes-version=${KUBERNETES_VERSION}
- run:
name: Wait for Minikube
command: |
JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}';
until kubectl get nodes -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do
sleep 1;
done
- run:
name: Watch pods
command: kubectl get pods -o json --watch
background: true
- run:
name: Watch nodes
command: kubectl get nodes -o json --watch
background: true
- restore_cache:
keys:
- e2e-{{ checksum "go.mod" }}-{{ checksum "go.sum" }}-2
- run:
name: Run the end-to-end test suite
command: |
mkdir $HOME/.go
export PATH=$HOME/.go/bin:${PATH}
curl -fsSL -o "/tmp/go.tar.gz" "https://dl.google.com/go/go1.15.6.linux-amd64.tar.gz"
tar -C $HOME/.go --strip-components=1 -xzf "/tmp/go.tar.gz"
go version
make e2e
- save_cache:
key: e2e-{{ checksum "go.mod" }}-{{ checksum "go.sum" }}-2
paths:
- "/home/circleci/go/pkg/mod"
- run:
name: Collect logs on failure from vkubelet-mock-0
command: |
kubectl logs vkubelet-mock-0
when: on_fail
workflows:
version: 2
validate_and_test:
jobs:
- validate
- test
- e2e:
requires:
- validate
- test

View File

@@ -1,4 +1,6 @@
.vscode
private.env
*.private.*
providers/azurebatch/deployment/
providers/azurebatch/deployment/
Dockerfile
.dockerignore

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

115
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,115 @@
name: CI
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [master]
pull_request:
permissions:
contents: read
env:
GO_VERSION: "1.23"
jobs:
lint:
name: Lint
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: false
- uses: actions/checkout@v4
- uses: golangci/golangci-lint-action@v8
with:
version: v2.1
args: --timeout=15m --config=.golangci.yml
skip-cache: true
unit-tests:
name: Unit Tests
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v4
- name: Run Tests
run: make test
env-tests:
name: Envtest Tests
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v4
- name: Run Tests
run: make envtest
e2e:
name: E2E
runs-on: ubuntu-22.04
timeout-minutes: 20
env:
CHANGE_MINIKUBE_NONE_USER: true
KUBERNETES_VERSION: v1.31
MINIKUBE_HOME: /home/runner
MINIKUBE_VERSION: v1.34.0
MINIKUBE_WANTUPDATENOTIFICATION: false
MINIKUBE_WANTREPORTERRORPROMPT: false
SKAFFOLD_VERSION: v2.13.2
GO111MODULE: "on"
steps:
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Skaffold
run: |
curl -sLo skaffold https://storage.googleapis.com/skaffold/releases/${SKAFFOLD_VERSION}/skaffold-linux-amd64
chmod +x skaffold
sudo mv skaffold /usr/local/bin/
echo /usr/local/bin >> $GITHUB_PATH
- name: Install Minikube dependencies
run: |
sudo apt-get update && sudo apt-get install -y apt-transport-https curl
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/${KUBERNETES_VERSION}/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list
curl -fsSL https://pkgs.k8s.io/core:/stable:/${KUBERNETES_VERSION}/deb/Release.key | sudo gpg --no-tty --yes --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
sudo apt-get update
sudo apt-get remove -y containerd.io containerd
sudo apt-get install -y kubectl docker.io
- name: Install Minikube
run: |
curl -sLo minikube https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-amd64
chmod +x minikube
sudo mv minikube /usr/local/bin/
- name: Start Minikube
run: |
sudo usermod -aG docker $USER && newgrp docker
minikube start --vm-driver=docker --cpus 2 --memory 2048 --kubernetes-version=${KUBERNETES_VERSION}
- name: Wait for Minikube
run: |
JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}';
until kubectl get nodes -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do
sleep 1;
done
- name: Run Tests
run: make e2e

View File

@@ -1,56 +1,59 @@
name: "CodeQL"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
branches: [master]
schedule:
- cron: '19 18 * * 3'
- cron: "19 18 * * 3"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
language: ["go"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

5
.gitignore vendored
View File

@@ -30,6 +30,9 @@ credentials.json
# Test loganalytics file
loganalytics.json
# Envtest
.envtest/
# VS Code files
.vscode/
@@ -41,3 +44,5 @@ loganalytics.json
**/terraform-provider-kubernetes
**/*.tfstate*
debug
vendor/

34
.golangci.bck.yml Normal file
View File

@@ -0,0 +1,34 @@
issues:
exclude-use-default: false
exclude:
# EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok
- Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). (is not checked|Errors unhandled)
exclude-dirs:
# This directory contains copy code from upstream kubernetes/kubernetes, skip it.
- internal/kubernetes
# This is mostly copied from upstream, rather than fixing that code here just ignore the errors.
- internal/podutils
linters:
enable:
- errcheck
- staticcheck
- unconvert
- gofmt
- goimports
- ineffassign
- govet
- unused
- misspell
- gosec
- copyloopvar # Checks for pointers to enclosing loop variables
- tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17
- lll
linters-settings:
gosec:
excludes:
- G304 # Potential file inclusion via variable
lll:
line-length: 200

View File

@@ -1,29 +1,51 @@
linter-settings:
lll:
line-length: 200
run:
skip-dirs:
# This directory contains copy code from upstream kubernetes/kubernetes, skip it.
- internal/kubernetes
version: "2"
linters:
enable:
- errcheck
- govet
- ineffassign
- golint
- goconst
- goimports
- unused
- varcheck
- deadcode
- copyloopvar
- gosec
- lll
- misspell
- nolintlint
- gocritic
- unconvert
settings:
gosec:
excludes:
- G304
lll:
line-length: 200
exclusions:
generated: lax
rules:
- path: (.+)\.go$
text: Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). (is not checked|Errors unhandled)
paths:
- internal/kubernetes
- internal/podutils
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- internal/kubernetes
- internal/podutils
- third_party$
- builtin$
- examples$
issues:
exclude-use-default: false
exclude:
# EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok
- Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked
# Maximum issues count per one linter.
# Set to 0 to disable.
# Default: 50
max-issues-per-linter: 0
# Maximum count of issues with the same text.
# Set to 0 to disable.
# Default: 3
max-same-issues: 0
# Make issues output unique by line.
# Default: true
uniq-by-line: false

View File

@@ -1,4 +1,6 @@
FROM golang:1.15 as builder
ARG GOLANG_CI_LINT_VERSION
FROM golang:1.23 as builder
ENV PATH /go/bin:/usr/local/go/bin:$PATH
ENV GOPATH /go
COPY . /go/src/github.com/virtual-kubelet/virtual-kubelet
@@ -7,6 +9,22 @@ ARG BUILD_TAGS=""
RUN make VK_BUILD_TAGS="${BUILD_TAGS}" build
RUN cp bin/virtual-kubelet /usr/bin/virtual-kubelet
FROM golangci/golangci-lint:${GOLANG_CI_LINT_VERSION} as lint
WORKDIR /app
COPY go.mod ./
COPY go.sum ./
RUN \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
ARG OUT_FORMAT
RUN \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/golangci-lint \
golangci-lint run -v --out-format="${OUT_FORMAT:-colored-line-number}"
FROM scratch
COPY --from=builder /usr/bin/virtual-kubelet /usr/bin/virtual-kubelet
COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs

View File

@@ -5,6 +5,8 @@ exec := $(DOCKER_IMAGE)
github_repo := virtual-kubelet/virtual-kubelet
binary := virtual-kubelet
GOTEST ?= go test $(if $V,-v)
export GO111MODULE ?= on
include Makefile.e2e
@@ -71,36 +73,28 @@ vet:
@echo "go vet'ing..."
ifndef CI
@echo "go vet'ing Outside CI..."
go vet $(allpackages)
go vet $(TESTDIRS)
else
@echo "go vet'ing in CI..."
mkdir -p test
( go vet $(allpackages); echo $$? ) | \
( go vet $(TESTDIRS); echo $$? ) | \
tee test/vet.txt | sed '$$ d'; exit $$(tail -1 test/vet.txt)
endif
test:
ifndef CI
@echo "Testing..."
go test $(if $V,-v) $(allpackages)
else
@echo "Testing in CI..."
mkdir -p test
( GODEBUG=cgocheck=2 go test -timeout=9m -v $(allpackages); echo $$? ) | \
tee test/output.txt | sed '$$ d'; exit $$(tail -1 test/output.txt)
endif
$(GOTEST) $(TESTDIRS)
list:
@echo "List..."
@echo $(allpackages)
@echo $(TESTDIRS)
cover: gocovmerge
@echo "Coverage Report..."
@echo "NOTE: make cover does not exit 1 on failure, don't use it to check for tests success!"
rm -f .GOPATH/cover/*.out cover/all.merged
$(if $V,@echo "-- go test -coverpkg=./... -coverprofile=cover/... ./...")
@for MOD in $(allpackages); do \
go test -coverpkg=`echo $(allpackages)|tr " " ","` \
@for MOD in $(TESTDIRS); do \
go test -coverpkg=`echo $(TESTDIRS)|tr " " ","` \
-coverprofile=cover/unit-`echo $$MOD|tr "/" "_"`.out \
$$MOD 2>&1 | grep -v "no packages being tested depend on"; \
done
@@ -142,11 +136,7 @@ VERSION := $(shell git describe --tags --always --dirty="-dev")
DATE := $(shell date -u '+%Y-%m-%d-%H:%M UTC')
VERSION_FLAGS := -ldflags='-X "main.buildVersion=$(VERSION)" -X "main.buildTime=$(DATE)"'
# assuming go 1.9 here!!
_allpackages = $(shell go list ./...)
# memoize allpackages, so that it's executed only once and only if used
allpackages = $(if $(__allpackages),,$(eval __allpackages := $$(_allpackages)))$(__allpackages)
TESTDIRS ?= ./...
.PHONY: goimports
goimports: $(gobin_tool)
@@ -175,24 +165,27 @@ authors:
rm -f NEWAUTHORS
rm -f GITAUTHORS
checksums_2.3.1.txt:
curl -o checksums_2.3.1.txt -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/checksums.txt
kubebuilder_2.3.1_${TEST_OS}_${TEST_ARCH}.tar.gz:
curl -C - -O -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_${TEST_OS}_${TEST_ARCH}.tar.gz
kubebuilder_2.3.1_${TEST_OS}_${TEST_ARCH}: kubebuilder_2.3.1_${TEST_OS}_${TEST_ARCH}.tar.gz checksums_2.3.1.txt
sha256sum -c --ignore-missing checksums_2.3.1.txt
tar -xvf kubebuilder_2.3.1_${TEST_OS}_${TEST_ARCH}.tar.gz
SETUP_ENVTEST_VERSION ?= v0.0.0-20250604165838-d6126d850224
ENVTEST_K8S_VERSION := 1.31.x
ENVTEST ?= go run sigs.k8s.io/controller-runtime/tools/setup-envtest@$(SETUP_ENVTEST_VERSION)
ENVTEST_DIR ?= $(shell pwd)/.envtest
export KUBEBUILDER_ASSETS ?= $(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(ENVTEST_DIR) -p path)
.PHONY: envtest
envtest: kubebuilder_2.3.1_${TEST_OS}_${TEST_ARCH}
envtest:
# You can add klog flags for debugging, like: -klog.v=10 -klog.logtostderr
# klogv2 flags just wraps our existing logrus.
KUBEBUILDER_ASSETS=$(PWD)/kubebuilder_2.3.1_${TEST_OS}_${TEST_ARCH}/bin go test -v -run=TestEnvtest ./node -envtest=true
$(GOTEST) -run=TestEnvtest ./node -envtest=true
.PHONY: fmt
fmt:
goimports -w $(shell go list -f '{{.Dir}}' ./...)
export GOLANG_CI_LINT_VERSION ?= v1.49.0
DOCKER_BUILD ?= docker buildx build
.PHONY: lint
lint: $(gobin_tool)
gobin -run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.33.0 run ./...
lint:
$(DOCKER_BUILD) --target=lint --build-arg GOLANG_CI_LINT_VERSION --build-arg OUT_FORMAT .

View File

@@ -1,7 +1,17 @@
# skaffold checks for kubectl context
# For minikube, docker-for-desktop and docker-desktop the context matches above names
# If one wants to use kind, kind gives context names based on the cluster-name
# But as of now they match the following syntax: kind-*
# The first check verifies that this is a kind kubernetes context
# Second check verifies the other ones
.PHONY: skaffold.validate
skaffold.validate: kubectl_context := $(shell kubectl config current-context)
skaffold.validate:
@if [[ ! "minikube,docker-for-desktop,docker-desktop" =~ .*"$(kubectl_context)".* ]]; then \
@if [[ "$(kubectl_context)" =~ .*"kind".* ]]; then \
true; \
elif [[ ! "minikube,docker-for-desktop,docker-desktop" =~ .*"$(kubectl_context)".* ]]; then \
echo current-context is [$(kubectl_context)]. Must be one of [minikube,docker-for-desktop,docker-desktop]; \
false; \
fi
@@ -39,7 +49,7 @@ e2e: NODE_NAME := vkubelet-mock-0
e2e: export VK_BUILD_TAGS += mock_provider
e2e: e2e.clean bin/e2e/virtual-kubelet skaffold/run
@echo Running tests...
cd $(PWD)/internal/test/e2e && go test -v -timeout 5m -tags e2e ./... \
cd $(PWD)/internal/test/e2e && $(GOTEST) -timeout 5m -tags e2e ./... \
-kubeconfig=$(KUBECONFIG) \
-namespace=$(NAMESPACE) \
-node-name=$(NODE_NAME)

View File

@@ -1,5 +1,7 @@
# Virtual Kubelet
[![Go Reference](https://pkg.go.dev/badge/github.com/virtual-kubelet/virtual-kubelet.svg)](https://pkg.go.dev/github.com/virtual-kubelet/virtual-kubelet)
Virtual Kubelet is an open source [Kubernetes kubelet](https://kubernetes.io/docs/reference/generated/kubelet/)
implementation that masquerades as a kubelet for the purposes of connecting Kubernetes to other APIs.
This allows the nodes to be backed by other services like ACI, AWS Fargate, [IoT Edge](https://github.com/Azure/iot-edge-virtual-kubelet-provider), [Tensile Kube](https://github.com/virtual-kubelet/tensile-kube) etc. The primary scenario for VK is enabling the extension of the Kubernetes API into serverless container platforms like ACI and Fargate, though we are open to others. However, it should be noted that VK is explicitly not intended to be an alternative to Kubernetes federation.
@@ -23,6 +25,8 @@ The best description is "Kubernetes API on top, programmable back."
+ [AWS Fargate Provider](#aws-fargate-provider)
+ [Elotl Kip](#elotl-kip)
+ [HashiCorp Nomad](#hashicorp-nomad-provider)
+ [InterLink](#interlink-provider)
+ [Liqo](#liqo-provider)
+ [OpenStack Zun](#openstack-zun-provider)
+ [Tensile Kube Provider](#tensile-kube-provider)
+ [Adding a New Provider via the Provider Interface](#adding-a-new-provider-via-the-provider-interface)
@@ -46,7 +50,7 @@ project to build a custom Kubernetes node agent.
See godoc for up to date instructions on consuming this project:
https://godoc.org/github.com/virtual-kubelet/virtual-kubelet
There are implementations available for several provides (listed above), see
There are implementations available for [several providers](#providers), see
those repos for details on how to deploy.
## Current Features
@@ -134,6 +138,20 @@ would on a Kubernetes node.
For detailed instructions, follow the guide [here](https://github.com/virtual-kubelet/nomad/blob/master/README.md).
### Interlink Provider
[interLink](https://intertwin-eu.github.io/interLink/) provides an abstraction for the execution of a Kubernetes pod on any remote resource that has the capability to manage a container's execution lifecycle. The use cases that drove the initial development of the tool are the Slurm-powered HPC centers, regardless the plugin based design is enabling several additional use cases to provide Kubernetes-API based access to infrastracture that cannot host a Kubelet processes.
InterLink is a Virtual Kubelet provider that can manage container lifecycle through a well defined API specification, allowing for any resource provider to be integrated with a simple http server and a handful of methods.
In other words, this is an attempt to streamline the process of creating custom Virtual Kubelet providers, avoiding the need for any resource provider to implement its own version of a Kubelet workflow, which would require having some domain expertise in the Kubernetes internals.
For detailed instruction, follow the guide [here](https://intertwin-eu.github.io/interLink/docs/category/guides).
### Liqo Provider
[Liqo](https://liqo.io) implements a provider for Virtual Kubelet designed to transparently offload pods and services to "peered" Kubernetes remote cluster. Liqo is capable of discovering neighbor clusters (using DNS, mDNS) and "peer" with them, or in other words, establish a relationship to share part of the cluster resources. When a cluster has established a peering, a new instance of the Liqo Virtual Kubelet is spawned to seamlessly extend the capacity of the cluster, by providing an abstraction of the resources of the remote cluster. The provider combined with the Liqo network fabric extends the cluster networking by enabling Pod-to-Pod traffic and multi-cluster east-west services, supporting endpoints on both clusters.
For detailed instruction, follow the guide [here](https://github.com/liqotech/liqo/blob/master/README.md)
### OpenStack Zun Provider
OpenStack [Zun](https://docs.openstack.org/zun/latest/) provider for Virtual Kubelet connects
@@ -162,12 +180,12 @@ performing the neccessary actions.
There are 3 main interfaces:
#### PodLifecylceHandler
#### PodLifecycleHandler
When pods are created, updated, or deleted from Kubernetes, these methods are
called to handle those actions.
[godoc#PodLifecylceHandler](https://godoc.org/github.com/virtual-kubelet/virtual-kubelet/node#PodLifecycleHandler)
[godoc#PodLifecycleHandler](https://godoc.org/github.com/virtual-kubelet/virtual-kubelet/node#PodLifecycleHandler)
```go
type PodLifecycleHandler interface {
@@ -264,6 +282,11 @@ One of the roles of a Kubelet is to accept requests from the API server for
things like `kubectl logs` and `kubectl exec`. Helpers for setting this up are
provided [here](https://godoc.org/github.com/virtual-kubelet/virtual-kubelet/node/api)
#### Scrape Pod metrics
If you want to use HPA(Horizontal Pod Autoscaler) in your cluster, the provider should implement the `GetStatsSummary` function. Then metrics-server will be able to get the metrics of the pods on virtual-kubelet. Otherwise, you may see `No metrics for pod ` on metrics-server, which means the metrics of the pods on virtual-kubelet are not collected.
## Testing
### Unit tests
@@ -293,9 +316,8 @@ Enable the ServiceNodeExclusion flag, by modifying the Controller Manager manife
Virtual Kubelet follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
Sign the [CNCF CLA](https://github.com/kubernetes/community/blob/master/CLA.md) to be able to make Pull Requests to this repo.
Monthly Virtual Kubelet Office Hours are held at 10am PST on the last Thursday of every month in this [zoom meeting room](https://zoom.us/j/94701509915). Check out the calendar [here](https://calendar.google.com/calendar?cid=bjRtbGMxYWNtNXR0NXQ1a2hqZmRkNTRncGNAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ).
Monthly Virtual Kubelet Office Hours are held at 10am PST on the second Thursday of every month in this [zoom meeting room](https://zoom.us/j/94701509915). Check out the calendar [here](https://calendar.google.com/calendar/embed?src=b119ced62134053de07d6c261b50d21ebde0da54f4163f5771b60ecf906e8b90%40group.calendar.google.com&ctz=America%2FLos_Angeles).
Our google drive with design specifications and meeting notes are [here](https://drive.google.com/drive/folders/19Ndu11WBCCBDowo9CrrGUHoIfd2L8Ueg?usp=sharing).
We also have a community slack channel named virtual-kubelet in the Kubernetes slack. You can also connect with the Virtual Kubelet community via the [mailing list](https://lists.cncf.io/g/virtualkubelet-dev).

View File

@@ -22,7 +22,7 @@ import (
"github.com/pkg/errors"
"github.com/spf13/pflag"
"k8s.io/klog"
klog "k8s.io/klog/v2"
)
type mapVar map[string]string
@@ -59,7 +59,13 @@ func (mv mapVar) Type() string {
func installFlags(flags *pflag.FlagSet, c *Opts) {
flags.StringVar(&c.KubeConfigPath, "kubeconfig", c.KubeConfigPath, "kube config file to use for connecting to the Kubernetes API server")
flags.StringVar(&c.KubeNamespace, "namespace", c.KubeNamespace, "kubernetes namespace (default is 'all')")
/* #nosec */
flags.MarkDeprecated("namespace", "Nodes must watch for pods in all namespaces. This option is now ignored.") //nolint:errcheck
/* #nosec */
flags.MarkHidden("namespace") //nolint:errcheck
flags.StringVar(&c.KubeClusterDomain, "cluster-domain", c.KubeClusterDomain, "kubernetes cluster-domain (default is 'cluster.local')")
flags.StringVar(&c.NodeName, "nodename", c.NodeName, "kubernetes node name")
flags.StringVar(&c.OperatingSystem, "os", c.OperatingSystem, "Operating System (Linux/Windows)")
@@ -68,11 +74,18 @@ func installFlags(flags *pflag.FlagSet, c *Opts) {
flags.StringVar(&c.MetricsAddr, "metrics-addr", c.MetricsAddr, "address to listen for metrics/stats requests")
flags.StringVar(&c.TaintKey, "taint", c.TaintKey, "Set node taint key")
flags.BoolVar(&c.DisableTaint, "disable-taint", c.DisableTaint, "disable the virtual-kubelet node taint")
/* #nosec */
flags.MarkDeprecated("taint", "Taint key should now be configured using the VK_TAINT_KEY environment variable") //nolint:errcheck
flags.IntVar(&c.PodSyncWorkers, "pod-sync-workers", c.PodSyncWorkers, `set the number of pod synchronization workers`)
flags.BoolVar(&c.EnableNodeLease, "enable-node-lease", c.EnableNodeLease, `use node leases (1.13) for node heartbeats`)
/* #nosec */
flags.MarkDeprecated("enable-node-lease", "leases are always enabled") //nolint:errcheck
/* #nosec */
flags.MarkHidden("enable-node-lease") //nolint:errcheck
flags.StringSliceVar(&c.TraceExporters, "trace-exporter", c.TraceExporters, fmt.Sprintf("sets the tracing exporter to use, available exporters: %s", AvailableTraceExporters()))
flags.StringVar(&c.TraceConfig.ServiceName, "trace-service-name", c.TraceConfig.ServiceName, "sets the name of the service used to register with the trace exporter")

View File

@@ -15,156 +15,22 @@
package root
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"os"
"time"
"github.com/pkg/errors"
"github.com/virtual-kubelet/virtual-kubelet/cmd/virtual-kubelet/internal/provider"
"github.com/virtual-kubelet/virtual-kubelet/log"
"github.com/virtual-kubelet/virtual-kubelet/node/api"
)
// AcceptedCiphers is the list of accepted TLS ciphers, with known weak ciphers elided
// Note this list should be a moving target.
var AcceptedCiphers = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
}
func loadTLSConfig(certPath, keyPath string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, errors.Wrap(err, "error loading tls certs")
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
CipherSuites: AcceptedCiphers,
}, nil
}
func setupHTTPServer(ctx context.Context, p provider.Provider, cfg *apiServerConfig, getPodsFromKubernetes api.PodListerFunc) (_ func(), retErr error) {
var closers []io.Closer
cancel := func() {
for _, c := range closers {
c.Close()
}
}
defer func() {
if retErr != nil {
cancel()
}
}()
if cfg.CertPath == "" || cfg.KeyPath == "" {
log.G(ctx).
WithField("certPath", cfg.CertPath).
WithField("keyPath", cfg.KeyPath).
Error("TLS certificates not provided, not setting up pod http server")
} else {
tlsCfg, err := loadTLSConfig(cfg.CertPath, cfg.KeyPath)
if err != nil {
return nil, err
}
l, err := tls.Listen("tcp", cfg.Addr, tlsCfg)
if err != nil {
return nil, errors.Wrap(err, "error setting up listener for pod http server")
}
mux := http.NewServeMux()
podRoutes := api.PodHandlerConfig{
RunInContainer: p.RunInContainer,
GetContainerLogs: p.GetContainerLogs,
GetPodsFromKubernetes: getPodsFromKubernetes,
GetPods: p.GetPods,
StreamIdleTimeout: cfg.StreamIdleTimeout,
StreamCreationTimeout: cfg.StreamCreationTimeout,
}
api.AttachPodRoutes(podRoutes, mux, true)
s := &http.Server{
Handler: mux,
TLSConfig: tlsCfg,
}
go serveHTTP(ctx, s, l, "pods")
closers = append(closers, s)
}
if cfg.MetricsAddr == "" {
log.G(ctx).Info("Pod metrics server not setup due to empty metrics address")
} else {
l, err := net.Listen("tcp", cfg.MetricsAddr)
if err != nil {
return nil, errors.Wrap(err, "could not setup listener for pod metrics http server")
}
mux := http.NewServeMux()
var summaryHandlerFunc api.PodStatsSummaryHandlerFunc
if mp, ok := p.(provider.PodMetricsProvider); ok {
summaryHandlerFunc = mp.GetStatsSummary
}
podMetricsRoutes := api.PodMetricsConfig{
GetStatsSummary: summaryHandlerFunc,
}
api.AttachPodMetricsRoutes(podMetricsRoutes, mux)
s := &http.Server{
Handler: mux,
}
go serveHTTP(ctx, s, l, "pod metrics")
closers = append(closers, s)
}
return cancel, nil
}
func serveHTTP(ctx context.Context, s *http.Server, l net.Listener, name string) {
if err := s.Serve(l); err != nil {
select {
case <-ctx.Done():
default:
log.G(ctx).WithError(err).Errorf("Error setting up %s http server", name)
}
}
l.Close()
}
type apiServerConfig struct {
CertPath string
KeyPath string
Addr string
MetricsAddr string
StreamIdleTimeout time.Duration
StreamCreationTimeout time.Duration
}
func getAPIConfig(c Opts) (*apiServerConfig, error) {
config := apiServerConfig{
CertPath: os.Getenv("APISERVER_CERT_LOCATION"),
KeyPath: os.Getenv("APISERVER_KEY_LOCATION"),
func getAPIConfig(c Opts) apiServerConfig {
return apiServerConfig{
Addr: fmt.Sprintf(":%d", c.ListenPort),
MetricsAddr: c.MetricsAddr,
StreamIdleTimeout: c.StreamIdleTimeout,
StreamCreationTimeout: c.StreamCreationTimeout,
}
config.Addr = fmt.Sprintf(":%d", c.ListenPort)
config.MetricsAddr = c.MetricsAddr
config.StreamIdleTimeout = c.StreamIdleTimeout
config.StreamCreationTimeout = c.StreamCreationTimeout
return &config, nil
}

View File

@@ -15,54 +15,10 @@
package root
import (
"context"
"strings"
"github.com/virtual-kubelet/virtual-kubelet/cmd/virtual-kubelet/internal/provider"
"github.com/virtual-kubelet/virtual-kubelet/errdefs"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const osLabel = "beta.kubernetes.io/os"
// NodeFromProvider builds a kubernetes node object from a provider
// This is a temporary solution until node stuff actually split off from the provider interface itself.
func NodeFromProvider(ctx context.Context, name string, taint *v1.Taint, p provider.Provider, version string) *v1.Node {
taints := make([]v1.Taint, 0)
if taint != nil {
taints = append(taints, *taint)
}
node := &v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: map[string]string{
"type": "virtual-kubelet",
"kubernetes.io/role": "agent",
"kubernetes.io/hostname": name,
},
},
Spec: v1.NodeSpec{
Taints: taints,
},
Status: v1.NodeStatus{
NodeInfo: v1.NodeSystemInfo{
Architecture: "amd64",
KubeletVersion: version,
},
},
}
p.ConfigureNode(ctx, node)
if _, ok := node.ObjectMeta.Labels[osLabel]; !ok {
node.ObjectMeta.Labels[osLabel] = strings.ToLower(node.Status.NodeInfo.OperatingSystem)
}
return node
}
// getTaint creates a taint using the provided key/value.
// Taint effect is read from the environment
// The taint key/value may be overwritten by the environment.
@@ -80,7 +36,7 @@ func getTaint(c Opts) (*corev1.Taint, error) {
key = getEnv("VKUBELET_TAINT_KEY", key)
value = getEnv("VKUBELET_TAINT_VALUE", value)
effectEnv := getEnv("VKUBELET_TAINT_EFFECT", string(c.TaintEffect))
effectEnv := getEnv("VKUBELET_TAINT_EFFECT", c.TaintEffect)
var effect corev1.TaintEffect
switch effectEnv {

View File

@@ -15,6 +15,7 @@
package root
import (
"fmt"
"os"
"path/filepath"
"strconv"
@@ -28,7 +29,7 @@ import (
// Defaults for root command options
const (
DefaultNodeName = "virtual-kubelet"
DefaultOperatingSystem = "Linux"
DefaultOperatingSystem = "linux"
DefaultInformerResyncPeriod = 1 * time.Minute
DefaultMetricsAddr = ":10255"
DefaultListenPort = 10250 // TODO(cpuguy83)(VK1.0): Change this to an addr instead of just a port.. we should not be listening on all interfaces.
@@ -95,6 +96,8 @@ type Opts struct {
Version string
}
const maxInt32 = 1<<31 - 1
// SetDefaultOpts sets default options for unset values on the passed in option struct.
// Fields tht are already set will not be modified.
func SetDefaultOpts(c *Opts) error {
@@ -128,6 +131,10 @@ func SetDefaultOpts(c *Opts) error {
if err != nil {
return errors.Wrap(err, "error parsing KUBELET_PORT environment variable")
}
if p > maxInt32 {
return fmt.Errorf("KUBELET_PORT environment variable is too large")
}
/* #nosec */
c.ListenPort = int32(p)
} else {
c.ListenPort = DefaultListenPort

View File

@@ -17,7 +17,7 @@ package root
import (
"context"
"os"
"path"
"runtime"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@@ -28,12 +28,6 @@ import (
"github.com/virtual-kubelet/virtual-kubelet/node"
"github.com/virtual-kubelet/virtual-kubelet/node/nodeutil"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubeinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/scheme"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/record"
)
// NewCommand creates a new top-level command.
@@ -75,33 +69,56 @@ func runRootCommand(ctx context.Context, s *provider.Store, c Opts) error {
}
}
client, err := nodeutil.ClientsetFromEnv(c.KubeConfigPath)
if err != nil {
return err
newProvider := func(cfg nodeutil.ProviderConfig) (nodeutil.Provider, node.NodeProvider, error) {
rm, err := manager.NewResourceManager(cfg.Pods, cfg.Secrets, cfg.ConfigMaps, cfg.Services)
if err != nil {
return nil, nil, errors.Wrap(err, "could not create resource manager")
}
initConfig := provider.InitConfig{
ConfigPath: c.ProviderConfigPath,
NodeName: c.NodeName,
OperatingSystem: c.OperatingSystem,
ResourceManager: rm,
DaemonPort: c.ListenPort,
InternalIP: os.Getenv("VKUBELET_POD_IP"),
KubeClusterDomain: c.KubeClusterDomain,
}
pInit := s.Get(c.Provider)
if pInit == nil {
return nil, nil, errors.Errorf("provider %q not found", c.Provider)
}
p, err := pInit(initConfig)
if err != nil {
return nil, nil, errors.Wrapf(err, "error initializing provider %s", c.Provider)
}
p.ConfigureNode(ctx, cfg.Node)
cfg.Node.Status.NodeInfo.KubeletVersion = c.Version
return p, nil, nil
}
// Create a shared informer factory for Kubernetes pods in the current namespace (if specified) and scheduled to the current node.
podInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(
client,
c.InformerResyncPeriod,
kubeinformers.WithNamespace(c.KubeNamespace),
nodeutil.PodInformerFilter(c.NodeName),
apiConfig := getAPIConfig(c)
cm, err := nodeutil.NewNode(c.NodeName, newProvider, func(cfg *nodeutil.NodeConfig) error {
cfg.KubeconfigPath = c.KubeConfigPath
cfg.InformerResyncPeriod = c.InformerResyncPeriod
if taint != nil {
cfg.NodeSpec.Spec.Taints = append(cfg.NodeSpec.Spec.Taints, *taint)
}
cfg.NodeSpec.Status.NodeInfo.Architecture = runtime.GOARCH
cfg.NodeSpec.Status.NodeInfo.OperatingSystem = c.OperatingSystem
cfg.HTTPListenAddr = apiConfig.Addr
cfg.StreamCreationTimeout = apiConfig.StreamCreationTimeout
cfg.StreamIdleTimeout = apiConfig.StreamIdleTimeout
cfg.DebugHTTP = true
cfg.NumWorkers = c.PodSyncWorkers
return nil
},
nodeutil.WithBootstrapFromRestConfig(),
)
podInformer := podInformerFactory.Core().V1().Pods()
// Create another shared informer factory for Kubernetes secrets and configmaps (not subject to any selectors).
scmInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(client, c.InformerResyncPeriod)
// Create a secret informer and a config map informer so we can pass their listers to the resource manager.
secretInformer := scmInformerFactory.Core().V1().Secrets()
configMapInformer := scmInformerFactory.Core().V1().ConfigMaps()
serviceInformer := scmInformerFactory.Core().V1().Services()
rm, err := manager.NewResourceManager(podInformer.Lister(), secretInformer.Lister(), configMapInformer.Lister(), serviceInformer.Lister())
if err != nil {
return errors.Wrap(err, "could not create resource manager")
}
apiConfig, err := getAPIConfig(c)
if err != nil {
return err
}
@@ -110,26 +127,6 @@ func runRootCommand(ctx context.Context, s *provider.Store, c Opts) error {
return err
}
initConfig := provider.InitConfig{
ConfigPath: c.ProviderConfigPath,
NodeName: c.NodeName,
OperatingSystem: c.OperatingSystem,
ResourceManager: rm,
DaemonPort: c.ListenPort,
InternalIP: os.Getenv("VKUBELET_POD_IP"),
KubeClusterDomain: c.KubeClusterDomain,
}
pInit := s.Get(c.Provider)
if pInit == nil {
return errors.Errorf("provider %q not found", c.Provider)
}
p, err := pInit(initConfig)
if err != nil {
return errors.Wrapf(err, "error initializing provider %s", c.Provider)
}
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(log.Fields{
"provider": c.Provider,
"operatingSystem": c.OperatingSystem,
@@ -137,117 +134,25 @@ func runRootCommand(ctx context.Context, s *provider.Store, c Opts) error {
"watchedNamespace": c.KubeNamespace,
}))
pNode := NodeFromProvider(ctx, c.NodeName, taint, p, c.Version)
np := node.NewNaiveNodeProvider()
additionalOptions := []node.NodeControllerOpt{
node.WithNodeStatusUpdateErrorHandler(func(ctx context.Context, err error) error {
if !k8serrors.IsNotFound(err) {
return err
}
go cm.Run(ctx) //nolint:errcheck
log.G(ctx).Debug("node not found")
newNode := pNode.DeepCopy()
newNode.ResourceVersion = ""
_, err = client.CoreV1().Nodes().Create(ctx, newNode, metav1.CreateOptions{})
if err != nil {
return err
}
log.G(ctx).Debug("created new node")
return nil
}),
}
if c.EnableNodeLease {
leaseClient := nodeutil.NodeLeaseV1Client(client)
// 40 seconds is the default lease time in upstream kubelet
additionalOptions = append(additionalOptions, node.WithNodeEnableLeaseV1(leaseClient, 40))
}
nodeRunner, err := node.NewNodeController(
np,
pNode,
client.CoreV1().Nodes(),
additionalOptions...,
)
if err != nil {
log.G(ctx).Fatal(err)
}
defer func() {
log.G(ctx).Debug("Waiting for controllers to be done")
cancel()
<-cm.Done()
}()
eb := record.NewBroadcaster()
eb.StartLogging(log.G(ctx).Infof)
eb.StartRecordingToSink(&corev1client.EventSinkImpl{Interface: client.CoreV1().Events(c.KubeNamespace)})
pc, err := node.NewPodController(node.PodControllerConfig{
PodClient: client.CoreV1(),
PodInformer: podInformer,
EventRecorder: eb.NewRecorder(scheme.Scheme, corev1.EventSource{Component: path.Join(pNode.Name, "pod-controller")}),
Provider: p,
SecretInformer: secretInformer,
ConfigMapInformer: configMapInformer,
ServiceInformer: serviceInformer,
})
if err != nil {
return errors.Wrap(err, "error setting up pod controller")
}
go podInformerFactory.Start(ctx.Done())
go scmInformerFactory.Start(ctx.Done())
cancelHTTP, err := setupHTTPServer(ctx, p, apiConfig, func(context.Context) ([]*corev1.Pod, error) {
return rm.GetPods(), nil
})
if err != nil {
log.G(ctx).Info("Waiting for controller to be ready")
if err := cm.WaitReady(ctx, c.StartupTimeout); err != nil {
return err
}
defer cancelHTTP()
go func() {
if err := pc.Run(ctx, c.PodSyncWorkers); err != nil && errors.Cause(err) != context.Canceled {
log.G(ctx).Fatal(err)
}
}()
log.G(ctx).Info("Ready")
if c.StartupTimeout > 0 {
ctx, cancel := context.WithTimeout(ctx, c.StartupTimeout)
log.G(ctx).Info("Waiting for pod controller / VK to be ready")
select {
case <-ctx.Done():
cancel()
return ctx.Err()
case <-pc.Ready():
}
cancel()
if err := pc.Err(); err != nil {
return err
}
select {
case <-ctx.Done():
case <-cm.Done():
return cm.Err()
}
go func() {
if err := nodeRunner.Run(ctx); err != nil {
log.G(ctx).Fatal(err)
}
}()
setNodeReady(pNode)
if err := np.UpdateStatus(ctx, pNode); err != nil {
return errors.Wrap(err, "error marking the node as ready")
}
log.G(ctx).Info("Initialized")
<-ctx.Done()
return nil
}
func setNodeReady(n *corev1.Node) {
for i, c := range n.Status.Conditions {
if c.Type != "Ready" {
continue
}
c.Message = "Kubelet is ready"
c.Reason = "KubeletReady"
c.Status = corev1.ConditionTrue
c.LastHeartbeatTime = metav1.Now()
c.LastTransitionTime = metav1.Now()
n.Status.Conditions[i] = c
return
}
}

View File

@@ -21,6 +21,7 @@ import (
"os"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/virtual-kubelet/virtual-kubelet/errdefs"
@@ -105,7 +106,8 @@ func setupZpages(ctx context.Context) {
zpages.Handle(mux, "/debug")
go func() {
// This should never terminate, if it does, it will always terminate with an error
e := http.Serve(listener, mux)
srv := &http.Server{Handler: mux, ReadHeaderTimeout: 30 * time.Second}
e := srv.Serve(listener)
if e == http.ErrServerClosed {
return
}

View File

@@ -19,7 +19,8 @@ import (
"go.opencensus.io/trace"
)
type TracingExporterOptions struct { // nolint: golint
// TracingExporterOptions is the options passed to the tracing exporter init function.
type TracingExporterOptions struct { //nolint: golint
Tags map[string]string
ServiceName string
}

View File

@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !no_jaeger_exporter
// +build !no_jaeger_exporter
package root
@@ -31,17 +32,17 @@ func init() {
// NewJaegerExporter creates a new opencensus tracing exporter.
func NewJaegerExporter(opts TracingExporterOptions) (trace.Exporter, error) {
jOpts := jaeger.Options{
Endpoint: os.Getenv("JAEGER_ENDPOINT"),
AgentEndpoint: os.Getenv("JAEGER_AGENT_ENDPOINT"),
Username: os.Getenv("JAEGER_USER"),
Password: os.Getenv("JAEGER_PASSWORD"),
CollectorEndpoint: os.Getenv("JAEGER_COLLECTOR_ENDPOINT"),
AgentEndpoint: os.Getenv("JAEGER_AGENT_ENDPOINT"),
Username: os.Getenv("JAEGER_USER"),
Password: os.Getenv("JAEGER_PASSWORD"),
Process: jaeger.Process{
ServiceName: opts.ServiceName,
},
}
if jOpts.Endpoint == "" && jOpts.AgentEndpoint == "" { // nolint:staticcheck
return nil, errors.New("Must specify either JAEGER_ENDPOINT or JAEGER_AGENT_ENDPOINT")
if jOpts.CollectorEndpoint == "" && jOpts.AgentEndpoint == "" { // nolintlint:staticcheck
return nil, errors.New("must specify either JAEGER_COLLECTOR_ENDPOINT or JAEGER_AGENT_ENDPOINT")
}
for k, v := range opts.Tags {

View File

@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !no_ocagent_exporter
// +build !no_ocagent_exporter
package root

View File

@@ -5,11 +5,12 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math/rand"
"os"
"strings"
"time"
dto "github.com/prometheus/client_model/go"
"github.com/virtual-kubelet/virtual-kubelet/errdefs"
"github.com/virtual-kubelet/virtual-kubelet/log"
"github.com/virtual-kubelet/virtual-kubelet/node/api"
@@ -17,7 +18,7 @@ import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
stats "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
)
const (
@@ -42,7 +43,7 @@ var (
*/
// MockProvider implements the virtual-kubelet provider interface and stores pods in memory.
type MockProvider struct { // nolint:golint
type MockProvider struct {
nodeName string
operatingSystem string
internalIP string
@@ -54,10 +55,12 @@ type MockProvider struct { // nolint:golint
}
// MockConfig contains a mock virtual-kubelet's configurable parameters.
type MockConfig struct { // nolint:golint
CPU string `json:"cpu,omitempty"`
Memory string `json:"memory,omitempty"`
Pods string `json:"pods,omitempty"`
type MockConfig struct {
CPU string `json:"cpu,omitempty"`
Memory string `json:"memory,omitempty"`
Pods string `json:"pods,omitempty"`
Others map[string]string `json:"others,omitempty"`
ProviderID string `json:"providerID,omitempty"`
}
// NewMockProviderMockConfig creates a new MockV0Provider. Mock legacy provider does not implement the new asynchronous podnotifier interface
@@ -97,9 +100,9 @@ func NewMockProvider(providerConfig, nodeName, operatingSystem string, internalI
// loadConfig loads the given json configuration files.
func loadConfig(providerConfig, nodeName string) (config MockConfig, err error) {
data, err := ioutil.ReadFile(providerConfig)
data, err := os.ReadFile(providerConfig)
if err != nil {
return config, err
return config, fmt.Errorf("error reaeding provider config: %w", err)
}
configMap := map[string]MockConfig{}
err = json.Unmarshal(data, &configMap)
@@ -120,13 +123,18 @@ func loadConfig(providerConfig, nodeName string) (config MockConfig, err error)
}
if _, err = resource.ParseQuantity(config.CPU); err != nil {
return config, fmt.Errorf("Invalid CPU value %v", config.CPU)
return config, fmt.Errorf("invalid CPU value %v", config.CPU)
}
if _, err = resource.ParseQuantity(config.Memory); err != nil {
return config, fmt.Errorf("Invalid memory value %v", config.Memory)
return config, fmt.Errorf("invalid memory value %v", config.Memory)
}
if _, err = resource.ParseQuantity(config.Pods); err != nil {
return config, fmt.Errorf("Invalid pods value %v", config.Pods)
return config, fmt.Errorf("invalid pods value %v", config.Pods)
}
for _, v := range config.Others {
if _, err = resource.ParseQuantity(v); err != nil {
return config, fmt.Errorf("invalid other value %v", v)
}
}
return config, nil
}
@@ -283,7 +291,7 @@ func (p *MockProvider) GetContainerLogs(ctx context.Context, namespace, podName,
ctx = addAttributes(ctx, span, namespaceKey, namespace, nameKey, podName, containerNameKey, containerName)
log.G(ctx).Infof("receive GetContainerLogs %q", podName)
return ioutil.NopCloser(strings.NewReader("")), nil
return io.NopCloser(strings.NewReader("")), nil
}
// RunInContainer executes a command in a container in the pod, copying data
@@ -293,6 +301,19 @@ func (p *MockProvider) RunInContainer(ctx context.Context, namespace, name, cont
return nil
}
// AttachToContainer attaches to the executing process of a container in the pod, copying data
// between in/out/err and the container's stdin/stdout/stderr.
func (p *MockProvider) AttachToContainer(ctx context.Context, namespace, name, container string, attach api.AttachIO) error {
log.G(ctx).Infof("receive AttachToContainer %q", container)
return nil
}
// PortForward forwards a local port to a port on the pod
func (p *MockProvider) PortForward(ctx context.Context, namespace, pod string, port int32, stream io.ReadWriteCloser) error {
log.G(ctx).Infof("receive PortForward %q", pod)
return nil
}
// GetPodStatus returns the status of a pod by name that is "running".
// returns nil if a pod by that name is not found.
func (p *MockProvider) GetPodStatus(ctx context.Context, namespace, name string) (*v1.PodStatus, error) {
@@ -328,10 +349,13 @@ func (p *MockProvider) GetPods(ctx context.Context) ([]*v1.Pod, error) {
return pods, nil
}
func (p *MockProvider) ConfigureNode(ctx context.Context, n *v1.Node) { // nolint:golint
ctx, span := trace.StartSpan(ctx, "mock.ConfigureNode") // nolint:staticcheck,ineffassign
func (p *MockProvider) ConfigureNode(ctx context.Context, n *v1.Node) {
ctx, span := trace.StartSpan(ctx, "mock.ConfigureNode") //nolint:staticcheck,ineffassign
defer span.End()
if p.config.ProviderID != "" {
n.Spec.ProviderID = p.config.ProviderID
}
n.Status.Capacity = p.capacity()
n.Status.Allocatable = p.capacity()
n.Status.Conditions = p.nodeConditions()
@@ -339,21 +363,25 @@ func (p *MockProvider) ConfigureNode(ctx context.Context, n *v1.Node) { // nolin
n.Status.DaemonEndpoints = p.nodeDaemonEndpoints()
os := p.operatingSystem
if os == "" {
os = "Linux"
os = "linux"
}
n.Status.NodeInfo.OperatingSystem = os
n.Status.NodeInfo.Architecture = "amd64"
n.ObjectMeta.Labels["alpha.service-controller.kubernetes.io/exclude-balancer"] = "true"
n.ObjectMeta.Labels["node.kubernetes.io/exclude-from-external-load-balancers"] = "true"
n.Labels["alpha.service-controller.kubernetes.io/exclude-balancer"] = "true"
n.Labels["node.kubernetes.io/exclude-from-external-load-balancers"] = "true"
}
// Capacity returns a resource list containing the capacity limits.
func (p *MockProvider) capacity() v1.ResourceList {
return v1.ResourceList{
rl := v1.ResourceList{
"cpu": resource.MustParse(p.config.CPU),
"memory": resource.MustParse(p.config.Memory),
"pods": resource.MustParse(p.config.Pods),
}
for k, v := range p.config.Others {
rl[v1.ResourceName(k)] = resource.MustParse(v)
}
return rl
}
// NodeConditions returns a list of conditions (Ready, OutOfDisk, etc), for updates to the node status
@@ -467,10 +495,14 @@ func (p *MockProvider) GetStatsSummary(ctx context.Context) (*stats.Summary, err
for _, container := range pod.Spec.Containers {
// Grab a dummy value to be used as the total CPU usage.
// The value should fit a uint32 in order to avoid overflows later on when computing pod stats.
/* #nosec */
dummyUsageNanoCores := uint64(rand.Uint32())
totalUsageNanoCores += dummyUsageNanoCores
// Create a dummy value to be used as the total RAM usage.
// The value should fit a uint32 in order to avoid overflows later on when computing pod stats.
/* #nosec */
dummyUsageBytes := uint64(rand.Uint32())
totalUsageBytes += dummyUsageBytes
// Append a ContainerStats object containing the dummy stats to the PodStats object.
@@ -504,6 +536,129 @@ func (p *MockProvider) GetStatsSummary(ctx context.Context) (*stats.Summary, err
return res, nil
}
func (p *MockProvider) generateMockMetrics(metricsMap map[string][]*dto.Metric, resourceType string, label []*dto.LabelPair) map[string][]*dto.Metric {
var (
cpuMetricSuffix = "_cpu_usage_seconds_total"
memoryMetricSuffix = "_memory_working_set_bytes"
dummyValue = float64(100)
)
if metricsMap == nil {
metricsMap = map[string][]*dto.Metric{}
}
finalCpuMetricName := resourceType + cpuMetricSuffix
finalMemoryMetricName := resourceType + memoryMetricSuffix
newCPUMetric := dto.Metric{
Label: label,
Counter: &dto.Counter{
Value: &dummyValue,
},
}
newMemoryMetric := dto.Metric{
Label: label,
Gauge: &dto.Gauge{
Value: &dummyValue,
},
}
// if metric family exists add to metric array
if cpuMetrics, ok := metricsMap[finalCpuMetricName]; ok {
metricsMap[finalCpuMetricName] = append(cpuMetrics, &newCPUMetric)
} else {
metricsMap[finalCpuMetricName] = []*dto.Metric{&newCPUMetric}
}
if memoryMetrics, ok := metricsMap[finalMemoryMetricName]; ok {
metricsMap[finalMemoryMetricName] = append(memoryMetrics, &newMemoryMetric)
} else {
metricsMap[finalMemoryMetricName] = []*dto.Metric{&newMemoryMetric}
}
return metricsMap
}
func (p *MockProvider) getMetricType(metricName string) *dto.MetricType {
var (
dtoCounterMetricType = dto.MetricType_COUNTER
dtoGaugeMetricType = dto.MetricType_GAUGE
cpuMetricSuffix = "_cpu_usage_seconds_total"
memoryMetricSuffix = "_memory_working_set_bytes"
)
if strings.HasSuffix(metricName, cpuMetricSuffix) {
return &dtoCounterMetricType
}
if strings.HasSuffix(metricName, memoryMetricSuffix) {
return &dtoGaugeMetricType
}
return nil
}
func (p *MockProvider) GetMetricsResource(ctx context.Context) ([]*dto.MetricFamily, error) {
var span trace.Span
ctx, span = trace.StartSpan(ctx, "GetMetricsResource") //nolint: ineffassign,staticcheck
defer span.End()
var (
nodeNameStr = "NodeName"
podNameStr = "PodName"
containerNameStr = "containerName"
)
nodeLabels := []*dto.LabelPair{
{
Name: &nodeNameStr,
Value: &p.nodeName,
},
}
metricsMap := p.generateMockMetrics(nil, "node", nodeLabels)
for _, pod := range p.pods {
podLabels := []*dto.LabelPair{
{
Name: &nodeNameStr,
Value: &p.nodeName,
},
{
Name: &podNameStr,
Value: &pod.Name,
},
}
metricsMap = p.generateMockMetrics(metricsMap, "pod", podLabels)
for _, container := range pod.Spec.Containers {
containerLabels := []*dto.LabelPair{
{
Name: &nodeNameStr,
Value: &p.nodeName,
},
{
Name: &podNameStr,
Value: &pod.Name,
},
{
Name: &containerNameStr,
Value: &container.Name,
},
}
metricsMap = p.generateMockMetrics(metricsMap, "container", containerLabels)
}
}
res := []*dto.MetricFamily{}
for metricName := range metricsMap {
tempName := metricName
tempMetrics := metricsMap[tempName]
metricFamily := dto.MetricFamily{
Name: &tempName,
Type: p.getMetricType(tempName),
Metric: tempMetrics,
}
res = append(res, &metricFamily)
}
return res, nil
}
// NotifyPods is called to set a pod notifier callback function. This should be called before any operations are done
// within the provider.
func (p *MockProvider) NotifyPods(ctx context.Context, notifier func(*v1.Pod)) {
@@ -516,15 +671,15 @@ func buildKeyFromNames(namespace string, name string) (string, error) {
// buildKey is a helper for building the "key" for the providers pod store.
func buildKey(pod *v1.Pod) (string, error) {
if pod.ObjectMeta.Namespace == "" {
if pod.Namespace == "" {
return "", fmt.Errorf("pod namespace not found")
}
if pod.ObjectMeta.Name == "" {
if pod.Name == "" {
return "", fmt.Errorf("pod name not found")
}
return buildKeyFromNames(pod.ObjectMeta.Namespace, pod.ObjectMeta.Name)
return buildKeyFromNames(pod.Namespace, pod.Name)
}
// addAttributes adds the specified attributes to the provided span.

View File

@@ -2,35 +2,15 @@ package provider
import (
"context"
"io"
"github.com/virtual-kubelet/virtual-kubelet/node"
"github.com/virtual-kubelet/virtual-kubelet/node/api"
"github.com/virtual-kubelet/virtual-kubelet/node/nodeutil"
v1 "k8s.io/api/core/v1"
stats "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
)
// Provider contains the methods required to implement a virtual-kubelet provider.
//
// Errors produced by these methods should implement an interface from
// github.com/virtual-kubelet/virtual-kubelet/errdefs package in order for the
// core logic to be able to understand the type of failure.
// Provider wraps the core provider type with an extra function needed to bootstrap the node
type Provider interface {
node.PodLifecycleHandler
// GetContainerLogs retrieves the logs of a container by name from the provider.
GetContainerLogs(ctx context.Context, namespace, podName, containerName string, opts api.ContainerLogOpts) (io.ReadCloser, error)
// RunInContainer executes a command in a container in the pod, copying data
// between in/out/err and the container's stdin/stdout/stderr.
RunInContainer(ctx context.Context, namespace, podName, containerName string, cmd []string, attach api.AttachIO) error
nodeutil.Provider
// ConfigureNode enables a provider to configure the node object that
// will be used for Kubernetes.
ConfigureNode(context.Context, *v1.Node)
}
// PodMetricsProvider is an optional interface that providers can implement to expose pod stats
type PodMetricsProvider interface {
GetStatsSummary(context.Context) (*stats.Summary, error)
}

View File

@@ -13,7 +13,7 @@ type Store struct {
ls map[string]InitFunc
}
func NewStore() *Store { // nolint:golint
func NewStore() *Store {
return &Store{
ls: make(map[string]InitFunc),
}
@@ -71,4 +71,4 @@ type InitConfig struct {
ResourceManager *manager.ResourceManager
}
type InitFunc func(InitConfig) (Provider, error) // nolint:golint
type InitFunc func(InitConfig) (Provider, error)

View File

@@ -2,12 +2,12 @@ package provider
const (
// OperatingSystemLinux is the configuration value for defining Linux.
OperatingSystemLinux = "Linux"
OperatingSystemLinux = "linux"
// OperatingSystemWindows is the configuration value for defining Windows.
OperatingSystemWindows = "Windows"
OperatingSystemWindows = "windows"
)
type OperatingSystems map[string]bool // nolint:golint
type OperatingSystems map[string]bool
var (
// ValidOperatingSystems defines the group of operating systems
@@ -18,7 +18,7 @@ var (
}
)
func (o OperatingSystems) Names() []string { // nolint:golint
func (o OperatingSystems) Names() []string {
keys := make([]string, 0, len(o))
for k := range o {
keys = append(keys, k)

View File

@@ -38,7 +38,7 @@ import (
var (
buildVersion = "N/A"
buildTime = "N/A"
k8sVersion = "v1.15.2" // This should follow the version of k8s.io/kubernetes we are importing
k8sVersion = "v1.31.4" // This should follow the version of k8s.io/client-go we are importing
)
func main() {

View File

@@ -6,6 +6,7 @@ import (
)
func registerMock(s *provider.Store) {
/* #nosec */
s.Register("mock", func(cfg provider.InitConfig) (provider.Provider, error) { //nolint:errcheck
return mock.NewMockProvider(
cfg.ConfigPath,

View File

@@ -0,0 +1,296 @@
# Virtual Kubelet Metrics Update
<!-- toc -->
- [Summary](#summary)
- [Motivation](#motivation)
- [Goals](#goals)
- [Non-Goals](#non-goals)
- [Proposal](#proposal)
- [Design Details](#design-details)
- [API](#api)
- [Data](#data)
- [Changes to the Provider](#changes-to-the-provider)
- [Test Plan](#test-plan)
<!-- /toc -->
## Summary
Add the new /metrics/resource endpoint in the virtual-kubelet to support the metrics server update for new Kubernetes versions `>=1.24`
## Motivation
The Kubernetes metrics server now tries to get metrics from the kubelet using the new metrics endpoint [/metrics/resource](https://github.com/kubernetes-sigs/metrics-server/commit/a2d732e5cdbfd93a6ebce221e8df0e8b463eecc6#diff-6e5b914d1403a14af1cc43582a2c9af727113037a3c6a77d8729aaefba084fb5R88),
while Virtual Kubelet is still exposing the earlier metrics endpoint [/stats/summary](https://github.com/virtual-kubelet/virtual-kubelet/blob/master/node/api/server.go#L90).
This causes metrics to break when using virtual kubelet with newer Kubernetes versions (>=1.24).
To support the new metrics server, this document proposes adding a new handler to handle the updated metrics endpoint.
This will be an additive update, and the old
[/stats/summary](https://github.com/virtual-kubelet/virtual-kubelet/blob/master/node/api/server.go#L90) endpoint will still be available to maintain backward compatibility with
the older metrics server version.
### Goals
- Support metrics for kubernetes version `>=1.24` through adding /metrics/resource endpoint handler.
### Non-Goals
- Ensure pod autoscaling works as expected with the newer kubernetes versions `>=1.24` as expected
## Proposal
Add a new handler for `/metrics/resource` endpoint that calls a new `GetMetricsResource` method in the provider,
which in-turn returns metrics using the prometheus `model.Samples` data structure as expected by the new metrics server.
The provider will need to implement the `GetMetricsResource` method in order to add support for the new `/metrics/resource` endpoint with Kubernetes version >=1.24
## Design Details
Currently the virtual kubelet code uses the `PodStatsSummaryHandler` method to set up a http handler for serving pod metrics via the `/stats/summary` endpoint.
To support the updated metrics server, we need to add another handler `PodMetricsResourceHandler` which can serve metrics via the `/metrics/resource` endpoint.
The `PodMetricsResourceHandler` calls the new `GetMetricsResource` method of the provider to get the metrics from the specific provider.
### API
Add `GetMetricsResource` to `PodHandlerConfig`
```go
type PodHandlerConfig struct { //nolint:golint
RunInContainer ContainerExecHandlerFunc
GetContainerLogs ContainerLogsHandlerFunc
// GetPods is meant to enumerate the pods that the provider knows about
GetPods PodListerFunc
// GetPodsFromKubernetes is meant to enumerate the pods that the node is meant to be running
GetPodsFromKubernetes PodListerFunc
GetStatsSummary PodStatsSummaryHandlerFunc
GetMetricsResource PodMetricsResourceHandlerFunc
StreamIdleTimeout time.Duration
StreamCreationTimeout time.Duration
}
```
Add endpoint to `PodHandler` method
```go
const MetricsResourceRouteSuffix = "/metrics/resource"
func PodHandler(p PodHandlerConfig, debug bool) http.Handler {
r := mux.NewRouter()
// This matches the behaviour in the reference kubelet
r.StrictSlash(true)
if debug {
r.HandleFunc("/runningpods/", HandleRunningPods(p.GetPods)).Methods("GET")
}
r.HandleFunc("/pods", HandleRunningPods(p.GetPodsFromKubernetes)).Methods("GET")
r.HandleFunc("/containerLogs/{namespace}/{pod}/{container}", HandleContainerLogs(p.GetContainerLogs)).Methods("GET")
r.HandleFunc(
"/exec/{namespace}/{pod}/{container}",
HandleContainerExec(
p.RunInContainer,
WithExecStreamCreationTimeout(p.StreamCreationTimeout),
WithExecStreamIdleTimeout(p.StreamIdleTimeout),
),
).Methods("POST", "GET")
if p.GetStatsSummary != nil {
f := HandlePodStatsSummary(p.GetStatsSummary)
r.HandleFunc("/stats/summary", f).Methods("GET")
r.HandleFunc("/stats/summary/", f).Methods("GET")
}
if p.GetMetricsResource != nil {
f := HandlePodMetricsResource(p.GetMetricsResource)
r.HandleFunc(MetricsResourceRouteSuffix, f).Methods("GET")
r.HandleFunc(MetricsResourceRouteSuffix+"/", f).Methods("GET")
}
r.NotFoundHandler = http.HandlerFunc(NotFound)
return r
}
```
New `PodMetricsResourceHandler` method, that uses the new `PodMetricsResourceHandlerFunc` definition.
```go
// PodMetricsResourceHandler creates an http handler for serving pod metrics.
//
// If the passed in handler func is nil this will create handlers which only
// serves http.StatusNotImplemented
func PodMetricsResourceHandler(f PodMetricsResourceHandlerFunc) http.Handler {
if f == nil {
return http.HandlerFunc(NotImplemented)
}
r := mux.NewRouter()
h := HandlePodMetricsResource(f)
r.Handle(MetricsResourceRouteSuffix, ochttp.WithRouteTag(h, "PodMetricsResourceHandler")).Methods("GET")
r.Handle(MetricsResourceRouteSuffix+"/", ochttp.WithRouteTag(h, "PodMetricsResourceHandler")).Methods("GET")
r.NotFoundHandler = http.HandlerFunc(NotFound)
return r
}
```
`HandlePodMetricsResource` method returns a HandlerFunc which serves the metrics encoded in prometheus' text format encoding as expected by the metrics-server
```go
// HandlePodMetricsResource makes an HTTP handler for implementing the kubelet /metrics/resource endpoint
func HandlePodMetricsResource(h PodMetricsResourceHandlerFunc) http.HandlerFunc {
if h == nil {
return NotImplemented
}
return handleError(func(w http.ResponseWriter, req *http.Request) error {
metrics, err := h(req.Context())
if err != nil {
if isCancelled(err) {
return err
}
return errors.Wrap(err, "error getting status from provider")
}
b, err := json.Marshal(metrics)
if err != nil {
return errors.Wrap(err, "error marshalling metrics")
}
if _, err := w.Write(b); err != nil {
return errors.Wrap(err, "could not write to client")
}
return nil
})
}
```
The `PodMetricsResourceHandlerFunc` returns the metrics data using Prometheus' `MetricFamily` data structure. More details are provided in the Data subsection
```go
// PodMetricsResourceHandlerFunc defines the handler for getting pod metrics
type PodMetricsResourceHandlerFunc func(context.Context) ([]*dto.MetricFamily, error)
```
### Data
The updated metrics server does not add any new fields to the metrics data but uses the Prometheus textparse series parser to parse and reconstruct the [MetricsBatch](https://github.com/kubernetes-sigs/metrics-server/blob/83b2e01f9825849ae5f562e47aa1a4178b5d06e5/pkg/storage/types.go#L31) data structure.
Currently virtual-kubelet is sending data to the server using the [summary](https://github.com/virtual-kubelet/virtual-kubelet/blob/be0a062aec9a5eeea3ad6fbe5aec557a235558f6/node/api/statsv1alpha1/types.go#L24) data structure. The Prometheus text parser expects a series of bytes as in the Prometheus [model.Samples](https://github.com/kubernetes/kubernetes/blob/a93eda9db305611cacd8b6ee930ab3149a08f9b0/vendor/github.com/prometheus/common/model/value.go#L184) data structure, similar to the test [here](https://github.com/prometheus/prometheus/blob/c70d85baed260f6013afd18d6cd0ffcac4339861/model/textparse/promparse_test.go#L31).
Examples of how the new metrics are defined may be seen in the Kubernetes e2e test that calls the /metrics/resource endpoint [here](https://github.com/kubernetes/kubernetes/blob/a93eda9db305611cacd8b6ee930ab3149a08f9b0/test/e2e_node/resource_metrics_test.go#L76), and the kubelet metrics defined in the Kubernetes/kubelet code [here](https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/metrics/collectors/resource_metrics.go) .
```go
var (
nodeCPUUsageDesc = metrics.NewDesc("node_cpu_usage_seconds_total",
"Cumulative cpu time consumed by the node in core-seconds",
nil,
nil,
metrics.ALPHA,
"")
nodeMemoryUsageDesc = metrics.NewDesc("node_memory_working_set_bytes",
"Current working set of the node in bytes",
nil,
nil,
metrics.ALPHA,
"")
containerCPUUsageDesc = metrics.NewDesc("container_cpu_usage_seconds_total",
"Cumulative cpu time consumed by the container in core-seconds",
[]string{"container", "pod", "namespace"},
nil,
metrics.ALPHA,
"")
containerMemoryUsageDesc = metrics.NewDesc("container_memory_working_set_bytes",
"Current working set of the container in bytes",
[]string{"container", "pod", "namespace"},
nil,
metrics.ALPHA,
"")
podCPUUsageDesc = metrics.NewDesc("pod_cpu_usage_seconds_total",
"Cumulative cpu time consumed by the pod in core-seconds",
[]string{"pod", "namespace"},
nil,
metrics.ALPHA,
"")
podMemoryUsageDesc = metrics.NewDesc("pod_memory_working_set_bytes",
"Current working set of the pod in bytes",
[]string{"pod", "namespace"},
nil,
metrics.ALPHA,
"")
resourceScrapeResultDesc = metrics.NewDesc("scrape_error",
"1 if there was an error while getting container metrics, 0 otherwise",
nil,
nil,
metrics.ALPHA,
"")
containerStartTimeDesc = metrics.NewDesc("container_start_time_seconds",
"Start time of the container since unix epoch in seconds",
[]string{"container", "pod", "namespace"},
nil,
metrics.ALPHA,
"")
)
```
The kubernetes/kubelet code implements Prometheus' [collector](https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/metrics/collectors/resource_metrics.go#L88) interface which is used along with the k8s.io/component-base implementation of the [registry](https://github.com/kubernetes/component-base/blob/40d14bdbd62f9e2ea697f97d81d4abc72839901e/metrics/registry.go#L114) interface in order to collect and return the metrics data using the Prometheus' [MetricFamily](https://github.com/prometheus/client_model/blob/master/go/metrics.pb.go#L773) data structure.
The Gather method in the registry calls the kubelet collector's Collect method, and returns the data using the MetricFamily data structure. The metrics server expects metrics to be encoded in prometheus'
text format, and the kubelet uses the http handler from prometheus' promhttp module which returns the metrics data encoded in prometheus' text format encoding.
```go
type KubeRegistry interface {
// Deprecated
RawMustRegister(...prometheus.Collector)
// CustomRegister is our internal variant of Prometheus registry.Register
CustomRegister(c StableCollector) error
// CustomMustRegister is our internal variant of Prometheus registry.MustRegister
CustomMustRegister(cs ...StableCollector)
// Register conforms to Prometheus registry.Register
Register(Registerable) error
// MustRegister conforms to Prometheus registry.MustRegister
MustRegister(...Registerable)
// Unregister conforms to Prometheus registry.Unregister
Unregister(collector Collector) bool
// Gather conforms to Prometheus gatherer.Gather
Gather() ([]*dto.MetricFamily, error)
// Reset invokes the Reset() function on all items in the registry
// which are added as resettables.
Reset()
}
```
Prometheus MetricsFamily data structure:
```go
type MetricFamily struct {
Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Help *string `protobuf:"bytes,2,opt,name=help" json:"help,omitempty"`
Type *MetricType `protobuf:"varint,3,opt,name=type,enum=io.prometheus.client.MetricType" json:"type,omitempty"`
Metric []*Metric `protobuf:"bytes,4,rep,name=metric" json:"metric,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
```
Therefore the provider's GetMetricsResource method should use the same return type as the Gather method in the registry interface.
### Changes to the Provider.
In order to support the new metrics endpoint the Provider must implement the GetMetricsResource method with definition
```golang
import (
dto "github.com/prometheus/client_model/go"
"context"
)
func GetMetricsResource(context.Context) ([]*dto.MetricsFamily, error) {
...
}
```
### Test Plan
- Write a provider implementation for GetMetricsResource method in ACI Provider and deploy pods get metrics using kubectl
- Run end-to-end tests with the provider implementation

33
errdefs/auth.go Normal file
View File

@@ -0,0 +1,33 @@
package errdefs
import (
"errors"
"fmt"
)
var (
// ErrForbidden is returned when the user is not authorized to perform the operation.
ErrForbidden = errors.New("forbidden")
// ErrUnauthorized is returned when the user is not authenticated.
ErrUnauthorized = errors.New("unauthorized")
)
// Unauthorized wraps ErrUnauthorized with a message.
func Unauthorized(msg string) error {
return fmt.Errorf("%w: %s", ErrUnauthorized, msg)
}
// Forbidden wraps ErrForbidden with a message.
func Forbidden(msg string) error {
return fmt.Errorf("%w: %s", ErrForbidden, msg)
}
// IsForbidden returns true if the error has ErrForbidden in the error chain.
func IsForbidden(err error) bool {
return errors.Is(err, ErrForbidden)
}
// IsUnauthorized returns true if the error has ErrUnauthorized in the error chain.
func IsUnauthorized(err error) bool {
return errors.Is(err, ErrUnauthorized)
}

183
go.mod
View File

@@ -1,76 +1,125 @@
module github.com/virtual-kubelet/virtual-kubelet
go 1.15
go 1.23.0
toolchain go1.23.4
require (
contrib.go.opencensus.io/exporter/jaeger v0.1.0
contrib.go.opencensus.io/exporter/ocagent v0.4.12
github.com/bombsimon/logrusr v1.0.0
github.com/docker/spdystream v0.0.0-20170912183627-bc6354cbbc29 // indirect
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 // indirect
github.com/google/go-cmp v0.4.0
github.com/gorilla/mux v1.7.0
contrib.go.opencensus.io/exporter/jaeger v0.2.1
contrib.go.opencensus.io/exporter/ocagent v0.7.0
github.com/bombsimon/logrusr/v3 v3.1.0
github.com/google/go-cmp v0.7.0
github.com/gorilla/mux v1.8.1
github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.8.1
github.com/prometheus/client_golang v1.0.0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/cobra v0.0.5
github.com/pkg/errors v0.9.1
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.64.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
go.opencensus.io v0.21.0
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
github.com/stretchr/testify v1.10.0
go.opencensus.io v0.24.0
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/sdk v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
golang.org/x/sync v0.14.0
golang.org/x/time v0.9.0
google.golang.org/protobuf v1.36.6
gotest.tools v2.2.0+incompatible
k8s.io/api v0.18.6
k8s.io/apimachinery v0.18.6
k8s.io/apiserver v0.18.4
k8s.io/client-go v0.18.6
k8s.io/klog v1.0.0
k8s.io/klog/v2 v2.0.0
k8s.io/kubernetes v1.18.4
k8s.io/utils v0.0.0-20200603063816-c1c6865ac451
sigs.k8s.io/controller-runtime v0.6.3
k8s.io/api v0.31.4
k8s.io/apimachinery v0.31.4
k8s.io/apiserver v0.31.4
k8s.io/client-go v0.31.4
k8s.io/klog/v2 v2.130.1
k8s.io/kubelet v0.31.4
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
sigs.k8s.io/controller-runtime v0.19.4
)
replace k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.18.4
replace k8s.io/cloud-provider => k8s.io/cloud-provider v0.18.4
replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.18.4
replace k8s.io/apiserver => k8s.io/apiserver v0.18.4
replace k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.18.4
replace k8s.io/cri-api => k8s.io/cri-api v0.18.4
replace k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.18.4
replace k8s.io/kubelet => k8s.io/kubelet v0.18.4
replace k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.18.4
replace k8s.io/apimachinery => k8s.io/apimachinery v0.18.4
replace k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.18.4
replace k8s.io/kube-proxy => k8s.io/kube-proxy v0.18.4
replace k8s.io/component-base => k8s.io/component-base v0.18.4
replace k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.18.4
replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.18.4
replace k8s.io/metrics => k8s.io/metrics v0.18.4
replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.18.4
replace k8s.io/code-generator => k8s.io/code-generator v0.18.4
replace k8s.io/client-go => k8s.io/client-go v0.18.4
replace k8s.io/kubectl => k8s.io/kubectl v0.18.4
replace k8s.io/api => k8s.io/api v0.18.4
require (
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // 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.4 // 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/cel-go v0.20.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/spdystream v0.4.0 // 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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/uber/jaeger-client-go v2.25.0+incompatible // indirect
github.com/x448/float16 v0.8.4 // indirect
go.etcd.io/etcd/api/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/v3 v3.5.14 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/api v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.65.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.31.0 // indirect
k8s.io/component-base v0.31.4 // indirect
k8s.io/kms v0.31.4 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

1305
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
FROM gcr.io/distroless/base
ENV APISERVER_CERT_LOCATION /vkubelet-mock-0-crt.pem
ENV APISERVER_KEY_LOCATION /vkubelet-mock-0-key.pem
ENV KUBELET_PORT 10250
ENV APISERVER_CERT_LOCATION=/vkubelet-mock-0-crt.pem
ENV APISERVER_KEY_LOCATION=/vkubelet-mock-0-key.pem
ENV KUBELET_PORT=10250
# Use the pre-built binary in "bin/virtual-kubelet".
COPY bin/e2e/virtual-kubelet /virtual-kubelet

View File

@@ -56,6 +56,14 @@ rules:
verbs:
- create
- patch
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- create
- update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@@ -4,6 +4,8 @@ metadata:
name: vkubelet-mock-0
spec:
containers:
- name: jaeger-tracing
image: jaegertracing/all-in-one:1.22
- name: vkubelet-mock-0
image: virtual-kubelet
# "IfNotPresent" is used to prevent Minikube from trying to pull from the registry (and failing) in the first place.
@@ -23,18 +25,16 @@ spec:
- --klog.logtostderr
- --log-level
- debug
- --trace-exporter
- jaeger
- --trace-sample-rate=always
env:
- name: JAEGER_AGENT_ENDPOINT
value: localhost:6831
- name: KUBELET_PORT
value: "10250"
- name: VKUBELET_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
ports:
- name: metrics
containerPort: 10255
readinessProbe:
httpGet:
path: /stats/summary
port: metrics
serviceAccountName: virtual-kubelet

View File

@@ -1,18 +1,17 @@
apiVersion: skaffold/v2beta10
apiVersion: skaffold/v4beta11
kind: Config
build:
artifacts:
- image: virtual-kubelet
docker:
# Use a Dockerfile specific for development only.
dockerfile: hack/skaffold/virtual-kubelet/Dockerfile
deploy:
kubectl:
manifests:
- image: virtual-kubelet
docker:
dockerfile: hack/skaffold/virtual-kubelet/Dockerfile
manifests:
rawYaml:
- hack/skaffold/virtual-kubelet/base.yml
- hack/skaffold/virtual-kubelet/pod.yml
deploy:
kubectl: {}
profiles:
- name: local
build:
# For the "local" profile, we must perform the build locally.
local: {}
- name: local
build:
local: {}

View File

@@ -2,24 +2,20 @@ package expansion
import (
"testing"
api "k8s.io/kubernetes/pkg/apis/core"
)
func TestMapReference(t *testing.T) {
envs := []api.EnvVar{
{
Name: "FOO",
Value: "bar",
},
{
Name: "ZOO",
Value: "$(FOO)-1",
},
{
Name: "BLU",
Value: "$(ZOO)-2",
},
// We use a struct here instead of a map because we need mappings to happen in order.
// Go maps are randomized.
type envVar struct {
Name string
Value string
}
envs := []envVar{
{"FOO", "bar"},
{"ZOO", "$(FOO)-1"},
{"BLU", "$(ZOO)-2"},
}
declaredEnv := map[string]string{

View File

@@ -0,0 +1,24 @@
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package portforward contains server-side logic for handling port forwarding requests.
package portforward
// ProtocolV1Name is the name of the subprotocol used for port forwarding.
const ProtocolV1Name = "portforward.k8s.io"
// SupportedProtocols are the supported port forwarding protocols.
var SupportedProtocols = []string{ProtocolV1Name}

View File

@@ -0,0 +1,317 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package portforward
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"sync"
"time"
api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/klog/v2"
)
func handleHTTPStreams(req *http.Request, w http.ResponseWriter, portForwarder PortForwarder, podName string, uid types.UID, supportedPortForwardProtocols []string, idleTimeout, streamCreationTimeout time.Duration) error {
_, err := httpstream.Handshake(req, w, supportedPortForwardProtocols)
// negotiated protocol isn't currently used server side, but could be in the future
if err != nil {
// Handshake writes the error to the client
return err
}
streamChan := make(chan httpstream.Stream, 1)
klog.V(5).InfoS("Upgrading port forward response")
// TODO aka-somix: SPDY is deprecated and it should be replaced in order to support HTTP/2
upgrader := spdy.NewResponseUpgrader()
conn := upgrader.UpgradeResponse(w, req, httpStreamReceived(streamChan))
if conn == nil {
return errors.New("unable to upgrade httpstream connection")
}
defer conn.Close()
klog.V(5).InfoS("Connection setting port forwarding streaming connection idle timeout", "connection", conn, "idleTimeout", idleTimeout)
conn.SetIdleTimeout(idleTimeout)
h := &httpStreamHandler{
conn: conn,
streamChan: streamChan,
streamPairs: make(map[string]*httpStreamPair),
streamCreationTimeout: streamCreationTimeout,
pod: podName,
uid: uid,
forwarder: portForwarder,
}
h.run()
return nil
}
// httpStreamReceived is the httpstream.NewStreamHandler for port
// forward streams. It checks each stream's port and stream type headers,
// rejecting any streams that with missing or invalid values. Each valid
// stream is sent to the streams channel.
func httpStreamReceived(streams chan httpstream.Stream) func(httpstream.Stream, <-chan struct{}) error {
return func(stream httpstream.Stream, replySent <-chan struct{}) error {
// make sure it has a valid port header
portString := stream.Headers().Get(api.PortHeader)
if len(portString) == 0 {
return fmt.Errorf("%q header is required", api.PortHeader)
}
port, err := strconv.ParseUint(portString, 10, 16)
if err != nil {
return fmt.Errorf("unable to parse %q as a port: %v", portString, err)
}
if port < 1 {
return fmt.Errorf("port %q must be > 0", portString)
}
// make sure it has a valid stream type header
streamType := stream.Headers().Get(api.StreamType)
if len(streamType) == 0 {
return fmt.Errorf("%q header is required", api.StreamType)
}
if streamType != api.StreamTypeError && streamType != api.StreamTypeData {
return fmt.Errorf("invalid stream type %q", streamType)
}
streams <- stream
return nil
}
}
// httpStreamHandler is capable of processing multiple port forward
// requests over a single httpstream.Connection.
type httpStreamHandler struct {
conn httpstream.Connection
streamChan chan httpstream.Stream
streamPairsLock sync.RWMutex
streamPairs map[string]*httpStreamPair
streamCreationTimeout time.Duration
pod string
uid types.UID
forwarder PortForwarder
}
// getStreamPair returns a httpStreamPair for requestID. This creates a
// new pair if one does not yet exist for the requestID. The returned bool is
// true if the pair was created.
func (h *httpStreamHandler) getStreamPair(requestID string) (*httpStreamPair, bool) {
h.streamPairsLock.Lock()
defer h.streamPairsLock.Unlock()
if p, ok := h.streamPairs[requestID]; ok {
klog.V(5).InfoS("Connection request found existing stream pair", "connection", h.conn, "request", requestID)
return p, false
}
klog.V(5).InfoS("Connection request creating new stream pair", "connection", h.conn, "request", requestID)
p := newPortForwardPair(requestID)
h.streamPairs[requestID] = p
return p, true
}
// monitorStreamPair waits for the pair to receive both its error and data
// streams, or for the timeout to expire (whichever happens first), and then
// removes the pair.
func (h *httpStreamHandler) monitorStreamPair(p *httpStreamPair, timeout <-chan time.Time) {
select {
case <-timeout:
err := fmt.Errorf("(conn=%v, request=%s) timed out waiting for streams", h.conn, p.requestID)
utilruntime.HandleError(err)
p.printError(err.Error())
case <-p.complete:
klog.V(5).InfoS("Connection request successfully received error and data streams", "connection", h.conn, "request", p.requestID)
}
h.removeStreamPair(p.requestID)
}
// hasStreamPair returns a bool indicating if a stream pair for requestID
// exists.
func (h *httpStreamHandler) hasStreamPair(requestID string) bool {
h.streamPairsLock.RLock()
defer h.streamPairsLock.RUnlock()
_, ok := h.streamPairs[requestID]
return ok
}
// removeStreamPair removes the stream pair identified by requestID from streamPairs.
func (h *httpStreamHandler) removeStreamPair(requestID string) {
h.streamPairsLock.Lock()
defer h.streamPairsLock.Unlock()
if h.conn != nil {
pair := h.streamPairs[requestID]
h.conn.RemoveStreams(pair.dataStream, pair.errorStream)
}
delete(h.streamPairs, requestID)
}
// requestID returns the request id for stream.
func (h *httpStreamHandler) requestID(stream httpstream.Stream) string {
requestID := stream.Headers().Get(api.PortForwardRequestIDHeader)
if len(requestID) == 0 {
klog.V(5).InfoS("Connection stream received without requestID header", "connection", h.conn)
// If we get here, it's because the connection came from an older client
// that isn't generating the request id header
// (https://github.com/kubernetes/kubernetes/blob/843134885e7e0b360eb5441e85b1410a8b1a7a0c/pkg/client/unversioned/portforward/portforward.go#L258-L287)
//
// This is a best-effort attempt at supporting older clients.
//
// When there aren't concurrent new forwarded connections, each connection
// will have a pair of streams (data, error), and the stream IDs will be
// consecutive odd numbers, e.g. 1 and 3 for the first connection. Convert
// the stream ID into a pseudo-request id by taking the stream type and
// using id = stream.Identifier() when the stream type is error,
// and id = stream.Identifier() - 2 when it's data.
//
// NOTE: this only works when there are not concurrent new streams from
// multiple forwarded connections; it's a best-effort attempt at supporting
// old clients that don't generate request ids. If there are concurrent
// new connections, it's possible that 1 connection gets streams whose IDs
// are not consecutive (e.g. 5 and 9 instead of 5 and 7).
streamType := stream.Headers().Get(api.StreamType)
switch streamType {
case api.StreamTypeError:
requestID = strconv.Itoa(int(stream.Identifier()))
case api.StreamTypeData:
requestID = strconv.Itoa(int(stream.Identifier()) - 2)
}
klog.V(5).InfoS("Connection automatically assigning request ID from stream type and stream ID", "connection", h.conn, "request", requestID, "streamType", streamType, "stream", stream.Identifier())
}
return requestID
}
// run is the main loop for the httpStreamHandler. It processes new
// streams, invoking portForward for each complete stream pair. The loop exits
// when the httpstream.Connection is closed.
func (h *httpStreamHandler) run() {
klog.V(5).InfoS("Connection waiting for port forward streams", "connection", h.conn)
Loop:
for {
select {
case <-h.conn.CloseChan():
klog.V(5).InfoS("Connection upgraded connection closed", "connection", h.conn)
break Loop
case stream := <-h.streamChan:
requestID := h.requestID(stream)
streamType := stream.Headers().Get(api.StreamType)
klog.V(5).InfoS("Connection request received new type of stream", "connection", h.conn, "request", requestID, "streamType", streamType)
p, created := h.getStreamPair(requestID)
if created {
go h.monitorStreamPair(p, time.After(h.streamCreationTimeout))
}
if complete, err := p.add(stream); err != nil {
msg := fmt.Sprintf("error processing stream for request %s: %v", requestID, err)
utilruntime.HandleError(errors.New(msg))
p.printError(msg)
} else if complete {
go h.portForward(p)
}
}
}
}
// portForward invokes the httpStreamHandler's forwarder.PortForward
// function for the given stream pair.
func (h *httpStreamHandler) portForward(p *httpStreamPair) {
ctx := context.Background()
defer p.dataStream.Close()
defer p.errorStream.Close()
portString := p.dataStream.Headers().Get(api.PortHeader)
port, _ := strconv.ParseInt(portString, 10, 32)
klog.V(5).InfoS("Connection request invoking forwarder.PortForward for port", "connection", h.conn, "request", p.requestID, "port", portString)
err := h.forwarder.PortForward(ctx, h.pod, h.uid, int32(port), p.dataStream)
klog.V(5).InfoS("Connection request done invoking forwarder.PortForward for port", "connection", h.conn, "request", p.requestID, "port", portString)
if err != nil {
msg := fmt.Errorf("error forwarding port %d to pod %s, uid %v: %v", port, h.pod, h.uid, err)
utilruntime.HandleError(msg)
fmt.Fprint(p.errorStream, msg.Error())
}
}
// httpStreamPair represents the error and data streams for a port
// forwarding request.
type httpStreamPair struct {
lock sync.RWMutex
requestID string
dataStream httpstream.Stream
errorStream httpstream.Stream
complete chan struct{}
}
// newPortForwardPair creates a new httpStreamPair.
func newPortForwardPair(requestID string) *httpStreamPair {
return &httpStreamPair{
requestID: requestID,
complete: make(chan struct{}),
}
}
// add adds the stream to the httpStreamPair. If the pair already
// contains a stream for the new stream's type, an error is returned. add
// returns true if both the data and error streams for this pair have been
// received.
func (p *httpStreamPair) add(stream httpstream.Stream) (bool, error) {
p.lock.Lock()
defer p.lock.Unlock()
switch stream.Headers().Get(api.StreamType) {
case api.StreamTypeError:
if p.errorStream != nil {
return false, errors.New("error stream already assigned")
}
p.errorStream = stream
case api.StreamTypeData:
if p.dataStream != nil {
return false, errors.New("data stream already assigned")
}
p.dataStream = stream
}
complete := p.errorStream != nil && p.dataStream != nil
if complete {
close(p.complete)
}
return complete, nil
}
// printError writes s to p.errorStream if p.errorStream has been set.
func (p *httpStreamPair) printError(s string) {
p.lock.RLock()
defer p.lock.RUnlock()
if p.errorStream != nil {
fmt.Fprint(p.errorStream, s)
}
}

View File

@@ -0,0 +1,267 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package portforward
import (
"net/http"
"testing"
"time"
api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/httpstream"
)
func TestHTTPStreamReceived(t *testing.T) {
tests := map[string]struct {
port string
streamType string
expectedError string
}{
"missing port": {
expectedError: `"port" header is required`,
},
"unable to parse port": {
port: "abc",
expectedError: `unable to parse "abc" as a port: strconv.ParseUint: parsing "abc": invalid syntax`,
},
"negative port": {
port: "-1",
expectedError: `unable to parse "-1" as a port: strconv.ParseUint: parsing "-1": invalid syntax`,
},
"missing stream type": {
port: "80",
expectedError: `"streamType" header is required`,
},
"valid port with error stream": {
port: "80",
streamType: "error",
},
"valid port with data stream": {
port: "80",
streamType: "data",
},
"invalid stream type": {
port: "80",
streamType: "foo",
expectedError: `invalid stream type "foo"`,
},
}
for name, test := range tests {
streams := make(chan httpstream.Stream, 1)
f := httpStreamReceived(streams)
stream := newFakeHTTPStream()
if len(test.port) > 0 {
stream.headers.Set("port", test.port)
}
if len(test.streamType) > 0 {
stream.headers.Set("streamType", test.streamType)
}
replySent := make(chan struct{})
err := f(stream, replySent)
close(replySent)
if len(test.expectedError) > 0 {
if err == nil {
t.Errorf("%s: expected err=%q, but it was nil", name, test.expectedError)
}
if e, a := test.expectedError, err.Error(); e != a {
t.Errorf("%s: expected err=%q, got %q", name, e, a)
}
continue
}
if err != nil {
t.Errorf("%s: unexpected error %v", name, err)
continue
}
if s := <-streams; s != stream {
t.Errorf("%s: expected stream %#v, got %#v", name, stream, s)
}
}
}
type fakeConn struct {
removeStreamsCalled bool
}
func (*fakeConn) CreateStream(headers http.Header) (httpstream.Stream, error) { return nil, nil }
func (*fakeConn) Close() error { return nil }
func (*fakeConn) CloseChan() <-chan bool { return nil }
func (*fakeConn) SetIdleTimeout(timeout time.Duration) {}
func (f *fakeConn) RemoveStreams(streams ...httpstream.Stream) { f.removeStreamsCalled = true }
func TestGetStreamPair(t *testing.T) {
timeout := make(chan time.Time)
conn := &fakeConn{}
h := &httpStreamHandler{
streamPairs: make(map[string]*httpStreamPair),
conn: conn,
}
// test adding a new entry
p, created := h.getStreamPair("1")
if p == nil {
t.Fatalf("unexpected nil pair")
}
if !created {
t.Fatal("expected created=true")
}
if p.dataStream != nil {
t.Errorf("unexpected non-nil data stream")
}
if p.errorStream != nil {
t.Errorf("unexpected non-nil error stream")
}
// start the monitor for this pair
monitorDone := make(chan struct{})
go func() {
h.monitorStreamPair(p, timeout)
close(monitorDone)
}()
if !h.hasStreamPair("1") {
t.Fatal("This should still be true")
}
// make sure we can retrieve an existing entry
p2, created := h.getStreamPair("1")
if created {
t.Fatal("expected created=false")
}
if p != p2 {
t.Fatalf("retrieving an existing pair: expected %#v, got %#v", p, p2)
}
// removed via complete
dataStream := newFakeHTTPStream()
dataStream.headers.Set(api.StreamType, api.StreamTypeData)
complete, err := p.add(dataStream)
if err != nil {
t.Fatalf("unexpected error adding data stream to pair: %v", err)
}
if complete {
t.Fatalf("unexpected complete")
}
errorStream := newFakeHTTPStream()
errorStream.headers.Set(api.StreamType, api.StreamTypeError)
complete, err = p.add(errorStream)
if err != nil {
t.Fatalf("unexpected error adding error stream to pair: %v", err)
}
if !complete {
t.Fatal("unexpected incomplete")
}
// make sure monitorStreamPair completed
<-monitorDone
if !conn.removeStreamsCalled {
t.Fatalf("connection remove stream not called")
}
conn.removeStreamsCalled = false
// make sure the pair was removed
if h.hasStreamPair("1") {
t.Fatal("expected removal of pair after both data and error streams received")
}
// removed via timeout
p, created = h.getStreamPair("2")
if !created {
t.Fatal("expected created=true")
}
if p == nil {
t.Fatal("expected p not to be nil")
}
monitorDone = make(chan struct{})
go func() {
h.monitorStreamPair(p, timeout)
close(monitorDone)
}()
// cause the timeout
close(timeout)
// make sure monitorStreamPair completed
<-monitorDone
if h.hasStreamPair("2") {
t.Fatal("expected stream pair to be removed")
}
if !conn.removeStreamsCalled {
t.Fatalf("connection remove stream not called")
}
}
func TestRequestID(t *testing.T) {
h := &httpStreamHandler{}
s := newFakeHTTPStream()
s.headers.Set(api.StreamType, api.StreamTypeError)
s.id = 1
if e, a := "1", h.requestID(s); e != a {
t.Errorf("expected %q, got %q", e, a)
}
s.headers.Set(api.StreamType, api.StreamTypeData)
s.id = 3
if e, a := "1", h.requestID(s); e != a {
t.Errorf("expected %q, got %q", e, a)
}
s.id = 7
s.headers.Set(api.PortForwardRequestIDHeader, "2")
if e, a := "2", h.requestID(s); e != a {
t.Errorf("expected %q, got %q", e, a)
}
}
type fakeHTTPStream struct {
headers http.Header
id uint32
}
func newFakeHTTPStream() *fakeHTTPStream {
return &fakeHTTPStream{
headers: make(http.Header),
}
}
var _ httpstream.Stream = &fakeHTTPStream{}
func (s *fakeHTTPStream) Read(data []byte) (int, error) {
return 0, nil
}
func (s *fakeHTTPStream) Write(data []byte) (int, error) {
return 0, nil
}
func (s *fakeHTTPStream) Close() error {
return nil
}
func (s *fakeHTTPStream) Reset() error {
return nil
}
func (s *fakeHTTPStream) Headers() http.Header {
return s.headers
}
func (s *fakeHTTPStream) Identifier() uint32 {
return s.id
}

View File

@@ -0,0 +1,54 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package portforward
import (
"context"
"io"
"net/http"
"time"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/util/wsstream"
)
// PortForwarder knows how to forward content from a data stream to/from a port
// in a pod.
type PortForwarder interface {
// PortForwarder copies data between a data stream and a port in a pod.
PortForward(ctx context.Context, name string, uid types.UID, port int32, stream io.ReadWriteCloser) error
}
// ServePortForward handles a port forwarding request. A single request is
// kept alive as long as the client is still alive and the connection has not
// been timed out due to idleness. This function handles multiple forwarded
// connections; i.e., multiple `curl http://localhost:8888/` requests will be
// handled by a single invocation of ServePortForward.
func ServePortForward(w http.ResponseWriter, req *http.Request, portForwarder PortForwarder, podName string, uid types.UID, portForwardOptions *V4Options, idleTimeout time.Duration, streamCreationTimeout time.Duration, supportedProtocols []string) {
var err error
if wsstream.IsWebSocketRequest(req) {
err = handleWebSocketStreams(req, w, portForwarder, podName, uid, portForwardOptions, supportedProtocols, idleTimeout, streamCreationTimeout)
} else {
err = handleHTTPStreams(req, w, portForwarder, podName, uid, supportedProtocols, idleTimeout, streamCreationTimeout)
}
if err != nil {
runtime.HandleError(err)
return
}
}

View File

@@ -0,0 +1,199 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package portforward
import (
"context"
"encoding/binary"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"sync"
"time"
"k8s.io/klog/v2"
api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/endpoints/responsewriter"
"k8s.io/apiserver/pkg/util/wsstream"
)
const (
dataChannel = iota
errorChannel
v4BinaryWebsocketProtocol = "v4." + wsstream.ChannelWebSocketProtocol
v4Base64WebsocketProtocol = "v4." + wsstream.Base64ChannelWebSocketProtocol
)
// V4Options contains details about which streams are required for port
// forwarding.
// All fields included in V4Options need to be expressed explicitly in the
// CRI (k8s.io/cri-api/pkg/apis/{version}/api.proto) PortForwardRequest.
type V4Options struct {
Ports []int32
}
// NewV4Options creates a new options from the Request.
func NewV4Options(req *http.Request) (*V4Options, error) {
if !wsstream.IsWebSocketRequest(req) {
return &V4Options{}, nil
}
portStrings := req.URL.Query()[api.PortHeader]
if len(portStrings) == 0 {
return nil, fmt.Errorf("query parameter %q is required", api.PortHeader)
}
ports := make([]int32, 0, len(portStrings))
for _, portString := range portStrings {
if len(portString) == 0 {
return nil, fmt.Errorf("query parameter %q cannot be empty", api.PortHeader)
}
for _, p := range strings.Split(portString, ",") {
port, err := strconv.ParseUint(p, 10, 16)
if err != nil {
return nil, fmt.Errorf("unable to parse %q as a port: %v", portString, err)
}
if port < 1 {
return nil, fmt.Errorf("port %q must be > 0", portString)
}
ports = append(ports, int32(port))
}
}
return &V4Options{
Ports: ports,
}, nil
}
// BuildV4Options returns a V4Options based on the given information.
func BuildV4Options(ports []int32) (*V4Options, error) {
return &V4Options{Ports: ports}, nil
}
// handleWebSocketStreams handles requests to forward ports to a pod via
// a PortForwarder. A pair of streams are created per port (DATA n,
// ERROR n+1). The associated port is written to each stream as a unsigned 16
// bit integer in little endian format.
func handleWebSocketStreams(req *http.Request, w http.ResponseWriter, portForwarder PortForwarder, podName string, uid types.UID, opts *V4Options, supportedPortForwardProtocols []string, idleTimeout, streamCreationTimeout time.Duration) error {
channels := make([]wsstream.ChannelType, 0, len(opts.Ports)*2)
for i := 0; i < len(opts.Ports); i++ {
channels = append(channels, wsstream.ReadWriteChannel, wsstream.WriteChannel)
}
conn := wsstream.NewConn(map[string]wsstream.ChannelProtocolConfig{
"": {
Binary: true,
Channels: channels,
},
v4BinaryWebsocketProtocol: {
Binary: true,
Channels: channels,
},
v4Base64WebsocketProtocol: {
Binary: false,
Channels: channels,
},
})
conn.SetIdleTimeout(idleTimeout)
_, streams, err := conn.Open(responsewriter.GetOriginal(w), req)
if err != nil {
err = fmt.Errorf("unable to upgrade websocket connection: %v", err)
return err
}
defer conn.Close()
streamPairs := make([]*websocketStreamPair, len(opts.Ports))
for i := range streamPairs {
streamPair := websocketStreamPair{
port: opts.Ports[i],
dataStream: streams[i*2+dataChannel],
errorStream: streams[i*2+errorChannel],
}
streamPairs[i] = &streamPair
portBytes := make([]byte, 2)
// port is always positive so conversion is allowable
binary.LittleEndian.PutUint16(portBytes, uint16(streamPair.port))
streamPair.dataStream.Write(portBytes)
streamPair.errorStream.Write(portBytes)
}
h := &websocketStreamHandler{
conn: conn,
streamPairs: streamPairs,
pod: podName,
uid: uid,
forwarder: portForwarder,
}
h.run()
return nil
}
// websocketStreamPair represents the error and data streams for a port
// forwarding request.
type websocketStreamPair struct {
port int32
dataStream io.ReadWriteCloser
errorStream io.WriteCloser
}
// websocketStreamHandler is capable of processing a single port forward
// request over a websocket connection
type websocketStreamHandler struct {
conn *wsstream.Conn
streamPairs []*websocketStreamPair
pod string
uid types.UID
forwarder PortForwarder
}
// run invokes the websocketStreamHandler's forwarder.PortForward
// function for the given stream pair.
func (h *websocketStreamHandler) run() {
wg := sync.WaitGroup{}
wg.Add(len(h.streamPairs))
for _, pair := range h.streamPairs {
p := pair
go func() {
defer wg.Done()
h.portForward(p)
}()
}
wg.Wait()
}
func (h *websocketStreamHandler) portForward(p *websocketStreamPair) {
ctx := context.Background()
defer p.dataStream.Close()
defer p.errorStream.Close()
klog.V(5).InfoS("Connection invoking forwarder.PortForward for port", "connection", h.conn, "port", p.port)
err := h.forwarder.PortForward(ctx, h.pod, h.uid, p.port, p.dataStream)
klog.V(5).InfoS("Connection done invoking forwarder.PortForward for port", "connection", h.conn, "port", p.port)
if err != nil {
msg := fmt.Errorf("error forwarding port %d to pod %s, uid %v: %v", p.port, h.pod, h.uid, err)
runtime.HandleError(msg)
fmt.Fprint(p.errorStream, msg.Error())
}
}

View File

@@ -0,0 +1,101 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package portforward
import (
"net/http"
"reflect"
"testing"
)
func TestV4Options(t *testing.T) {
tests := map[string]struct {
url string
websocket bool
expectedOpts *V4Options
expectedError string
}{
"non-ws request": {
url: "http://example.com",
expectedOpts: &V4Options{},
},
"missing port": {
url: "http://example.com",
websocket: true,
expectedError: `query parameter "port" is required`,
},
"unable to parse port": {
url: "http://example.com?port=abc",
websocket: true,
expectedError: `unable to parse "abc" as a port: strconv.ParseUint: parsing "abc": invalid syntax`,
},
"negative port": {
url: "http://example.com?port=-1",
websocket: true,
expectedError: `unable to parse "-1" as a port: strconv.ParseUint: parsing "-1": invalid syntax`,
},
"one port": {
url: "http://example.com?port=80",
websocket: true,
expectedOpts: &V4Options{
Ports: []int32{80},
},
},
"multiple ports": {
url: "http://example.com?port=80,90,100",
websocket: true,
expectedOpts: &V4Options{
Ports: []int32{80, 90, 100},
},
},
"multiple port": {
url: "http://example.com?port=80&port=90",
websocket: true,
expectedOpts: &V4Options{
Ports: []int32{80, 90},
},
},
}
for name, test := range tests {
req, err := http.NewRequest(http.MethodGet, test.url, nil)
if err != nil {
t.Errorf("%s: invalid url %q err=%q", name, test.url, err)
continue
}
if test.websocket {
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "websocket")
}
opts, err := NewV4Options(req)
if len(test.expectedError) > 0 {
if err == nil {
t.Errorf("%s: expected err=%q, but it was nil", name, test.expectedError)
}
if e, a := test.expectedError, err.Error(); e != a {
t.Errorf("%s: expected err=%q, got %q", name, e, a)
}
continue
}
if err != nil {
t.Errorf("%s: unexpected error %v", name, err)
continue
}
if !reflect.DeepEqual(test.expectedOpts, opts) {
t.Errorf("%s: expected options %#v, got %#v", name, test.expectedOpts, err)
}
}
}

View File

@@ -0,0 +1,79 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package remotecommand
import (
"fmt"
"io"
"net/http"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/tools/remotecommand"
utilexec "k8s.io/utils/exec"
)
// Attacher knows how to attach to a container in a pod.
type Attacher interface {
// AttachToContainer attaches to a container in the pod, copying data
// between in/out/err and the container's stdin/stdout/stderr.
AttachToContainer(name string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error
}
// ServeAttach handles requests to attach to a container. After
// creating/receiving the required streams, it delegates the actual attachment
// to the attacher.
func ServeAttach(w http.ResponseWriter, req *http.Request, attacher Attacher, podName string, uid types.UID, container string, streamOpts *Options, idleTimeout, streamCreationTimeout time.Duration, supportedProtocols []string) {
ctx, ok := createStreams(req, w, streamOpts, supportedProtocols, idleTimeout, streamCreationTimeout)
if !ok {
// error is handled by createStreams
return
}
defer ctx.conn.Close()
err := attacher.AttachToContainer(podName, uid, container, ctx.stdinStream, ctx.stdoutStream, ctx.stderrStream, ctx.tty, ctx.resizeChan, 0)
if err != nil {
if exitErr, ok := err.(utilexec.ExitError); ok && exitErr.Exited() {
rc := exitErr.ExitStatus()
ctx.writeStatus(&apierrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Reason: remotecommandconsts.NonZeroExitCodeReason,
Details: &metav1.StatusDetails{
Causes: []metav1.StatusCause{
{
Type: remotecommandconsts.ExitCodeCauseType,
Message: fmt.Sprintf("%d", rc),
},
},
},
Message: fmt.Sprintf("command terminated with non-zero exit code: %v", exitErr),
}})
return
}
err = fmt.Errorf("error attaching to container: %v", err)
runtime.HandleError(err)
ctx.writeStatus(apierrors.NewInternalError(err))
return
}
ctx.writeStatus(&apierrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusSuccess,
}})
}

View File

@@ -148,6 +148,8 @@ func createHTTPStreamStreams(req *http.Request, w http.ResponseWriter, opts *Opt
var handler protocolHandler
switch protocol {
case remotecommandconsts.StreamProtocolV5Name:
handler = &v5ProtocolHandler{}
case remotecommandconsts.StreamProtocolV4Name:
handler = &v4ProtocolHandler{}
case remotecommandconsts.StreamProtocolV3Name:
@@ -159,6 +161,9 @@ func createHTTPStreamStreams(req *http.Request, w http.ResponseWriter, opts *Opt
fallthrough
case remotecommandconsts.StreamProtocolV1Name:
handler = &v1ProtocolHandler{}
default:
klog.Errorf("unable to create HTTP stream: unknown protocol %q", protocol)
return nil, false
}
// count the streams client asked for, starting with 1
@@ -199,6 +204,57 @@ type protocolHandler interface {
supportsTerminalResizing() bool
}
// v5ProtocolHandler implements the V5 protocol version for streaming command execution.
type v5ProtocolHandler struct{}
func (*v5ProtocolHandler) waitForStreams(streams <-chan streamAndReply, expectedStreams int, expired <-chan time.Time) (*context, error) {
ctx := &context{}
receivedStreams := 0
replyChan := make(chan struct{})
stop := make(chan struct{})
defer close(stop)
WaitForStreams:
for {
select {
case stream := <-streams:
streamType := stream.Headers().Get(api.StreamType)
switch streamType {
case api.StreamTypeError:
ctx.writeStatus = v4WriteStatusFunc(stream) // write json errors
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStdin:
ctx.stdinStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStdout:
ctx.stdoutStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStderr:
ctx.stderrStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeResize:
ctx.resizeStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
default:
runtime.HandleError(fmt.Errorf("unexpected stream type: %q", streamType))
}
case <-replyChan:
receivedStreams++
if receivedStreams == expectedStreams {
break WaitForStreams
}
case <-expired:
// TODO find a way to return the error to the user. Maybe use a separate
// stream to report errors?
return nil, errors.New("timed out waiting for client to create streams")
}
}
return ctx, nil
}
// supportsTerminalResizing returns true because v5ProtocolHandler supports it
func (*v5ProtocolHandler) supportsTerminalResizing() bool { return true }
// v4ProtocolHandler implements the V4 protocol version for streaming command execution. It only differs
// in from v3 in the error stream format using an json-marshaled metav1.Status which carries
// the process' exit code.

View File

@@ -1,9 +0,0 @@
package lockdeps
import (
// TODO(Sargun): Remove in Go1.13
// This is a dep that `go mod tidy` keeps removing, because it's a transitive dep that's pulled in via a test
// See: https://github.com/golang/go/issues/29702
_ "github.com/prometheus/client_golang/prometheus"
_ "golang.org/x/sys/unix"
)

View File

@@ -32,7 +32,12 @@ type ResourceManager struct {
}
// NewResourceManager returns a ResourceManager with the internal maps initialized.
func NewResourceManager(podLister corev1listers.PodLister, secretLister corev1listers.SecretLister, configMapLister corev1listers.ConfigMapLister, serviceLister corev1listers.ServiceLister) (*ResourceManager, error) {
func NewResourceManager(
podLister corev1listers.PodLister,
secretLister corev1listers.SecretLister,
configMapLister corev1listers.ConfigMapLister,
serviceLister corev1listers.ServiceLister,
) (*ResourceManager, error) {
rm := ResourceManager{
podLister: podLister,
secretLister: secretLister,

View File

@@ -123,7 +123,7 @@ func TestGetConfigMap(t *testing.T) {
}
value := configMap.Data["key-0"]
if value != "val-0" {
t.Fatal("got unexpected value", string(value))
t.Fatal("got unexpected value", value)
}
// Try to get a configmap that does not exist, and make sure we've got a "not found" error as a response.

View File

@@ -0,0 +1,15 @@
Much of this is copied from k8s.io/kubernetes, even if it isn't a 1-1 copy of a
file. This exists so we do not have to import from k8s.io/kubernetes which is
currently problematic. Ideally most or all of this will go away and an upstream
solution is found so that we can share an implementation with Kubelet without
importing from k8s.io/kubernetes
| filename | upstream location |
|----------|-------------------|
| envvars.go | https://github.com/kubernetes/kubernetes/blob/98d5dc5d36d34a7ee13368a7893dcb400ec4e566/pkg/kubelet/envvars/envvars.go#L32 |
| helper.go#ConvertDownwardAPIFieldLabel | https://github.com/kubernetes/kubernetes/blob/98d5dc5d36d34a7ee13368a7893dcb400ec4e566/pkg/apis/core/pods/helpers.go#L65 |
| helper.go#ExtractFieldPathAsString | https://github.com/kubernetes/kubernetes/blob/98d5dc5d36d34a7ee13368a7893dcb400ec4e566/pkg/fieldpath/fieldpath.go#L46 |
| helper.go#SplitMaybeSubscriptedPath | https://github.com/kubernetes/kubernetes/blob/98d5dc5d36d34a7ee13368a7893dcb400ec4e566/pkg/fieldpath/fieldpath.go#L96 |
| helper.go#FormatMap | https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/pkg/fieldpath/fieldpath.go#L29 |
| helper.go#IsServiceIPSet | https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/pkg/apis/core/v1/helper/helpers.go#L139 |

View File

@@ -29,11 +29,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
apivalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/tools/record"
podshelper "k8s.io/kubernetes/pkg/apis/core/pods"
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
fieldpath "k8s.io/kubernetes/pkg/fieldpath"
"k8s.io/kubernetes/pkg/kubelet/envvars"
"k8s.io/utils/pointer"
"k8s.io/utils/ptr"
)
const (
@@ -139,7 +135,7 @@ func getServiceEnvVarMap(rm *manager.ResourceManager, ns string, enableServiceLi
for i := range services {
service := services[i]
// ignore services where ClusterIP is "None" or empty
if !v1helper.IsServiceIPSet(service) {
if !IsServiceIPSet(service) {
continue
}
serviceName := service.Name
@@ -162,7 +158,7 @@ func getServiceEnvVarMap(rm *manager.ResourceManager, ns string, enableServiceLi
mappedServices = append(mappedServices, serviceMap[key])
}
for _, e := range envvars.FromServices(mappedServices) {
for _, e := range FromServices(mappedServices) {
m[e.Name] = e.Value
}
return m, nil
@@ -338,7 +334,7 @@ func getEnvironmentVariableValue(ctx context.Context, env *corev1.EnvVar, mappin
return getEnvironmentVariableValueWithValueFrom(ctx, env, mappingFunc, pod, container, rm, recorder)
}
// Handle values that have been directly provided after expanding variable references.
return pointer.StringPtr(expansion.Expand(env.Value, mappingFunc)), nil
return ptr.To(expansion.Expand(env.Value, mappingFunc)), nil
}
func getEnvironmentVariableValueWithValueFrom(ctx context.Context, env *corev1.EnvVar, mappingFunc func(string) string, pod *corev1.Pod, container *corev1.Container, rm *manager.ResourceManager, recorder record.EventRecorder) (*string, error) {
@@ -415,7 +411,7 @@ func getEnvironmentVariableValueWithValueFromConfigMapKeyRef(ctx context.Context
return nil, fmt.Errorf("configmap %q doesn't contain the %q key required by pod %s", vf.Name, vf.Key, pod.Name)
}
// Populate the environment variable and continue on to the next reference.
return pointer.StringPtr(keyValue), nil
return ptr.To(keyValue), nil
}
func getEnvironmentVariableValueWithValueFromSecretKeyRef(ctx context.Context, env *corev1.EnvVar, mappingFunc func(string) string, pod *corev1.Pod, container *corev1.Container, rm *manager.ResourceManager, recorder record.EventRecorder) (*string, error) {
@@ -467,7 +463,7 @@ func getEnvironmentVariableValueWithValueFromSecretKeyRef(ctx context.Context, e
return nil, fmt.Errorf("secret %q doesn't contain the %q key required by pod %s", vf.Name, vf.Key, pod.Name)
}
// Populate the environment variable and continue on to the next reference.
return pointer.StringPtr(string(keyValue)), nil
return ptr.To(string(keyValue)), nil
}
// Handle population from a field (downward API).
@@ -480,13 +476,13 @@ func getEnvironmentVariableValueWithValueFromFieldRef(ctx context.Context, env *
return nil, err
}
return pointer.StringPtr(runtimeVal), nil
return ptr.To(runtimeVal), nil
}
// podFieldSelectorRuntimeValue returns the runtime value of the given
// selector for a pod.
func podFieldSelectorRuntimeValue(fs *corev1.ObjectFieldSelector, pod *corev1.Pod) (string, error) {
internalFieldPath, _, err := podshelper.ConvertDownwardAPIFieldLabel(fs.APIVersion, fs.FieldPath, "")
internalFieldPath, _, err := ConvertDownwardAPIFieldLabel(fs.APIVersion, fs.FieldPath, "")
if err != nil {
return "", err
}
@@ -497,5 +493,5 @@ func podFieldSelectorRuntimeValue(fs *corev1.ObjectFieldSelector, pod *corev1.Po
return pod.Spec.ServiceAccountName, nil
}
return fieldpath.ExtractFieldPathAsString(pod, internalFieldPath)
return ExtractFieldPathAsString(pod, internalFieldPath)
}

View File

@@ -0,0 +1,112 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package podutils
import (
"fmt"
"net"
"strconv"
"strings"
v1 "k8s.io/api/core/v1"
)
// FromServices builds environment variables that a container is started with,
// which tell the container where to find the services it may need, which are
// provided as an argument.
func FromServices(services []*v1.Service) []v1.EnvVar {
var result []v1.EnvVar
for i := range services {
service := services[i]
// ignore services where ClusterIP is "None" or empty
// the services passed to this method should be pre-filtered
// only services that have the cluster IP set should be included here
if !IsServiceIPSet(service) {
continue
}
// Host
name := makeEnvVariableName(service.Name) + "_SERVICE_HOST"
result = append(result, v1.EnvVar{Name: name, Value: service.Spec.ClusterIP})
// First port - give it the backwards-compatible name
name = makeEnvVariableName(service.Name) + "_SERVICE_PORT"
result = append(result, v1.EnvVar{Name: name, Value: strconv.Itoa(int(service.Spec.Ports[0].Port))})
// All named ports (only the first may be unnamed, checked in validation)
for i := range service.Spec.Ports {
sp := &service.Spec.Ports[i]
if sp.Name != "" {
pn := name + "_" + makeEnvVariableName(sp.Name)
result = append(result, v1.EnvVar{Name: pn, Value: strconv.Itoa(int(sp.Port))})
}
}
// Docker-compatible vars.
result = append(result, makeLinkVariables(service)...)
}
return result
}
func makeEnvVariableName(str string) string {
// TODO: If we simplify to "all names are DNS1123Subdomains" this
// will need two tweaks:
// 1) Handle leading digits
// 2) Handle dots
return strings.ToUpper(strings.Replace(str, "-", "_", -1))
}
func makeLinkVariables(service *v1.Service) []v1.EnvVar {
prefix := makeEnvVariableName(service.Name)
all := []v1.EnvVar{}
for i := range service.Spec.Ports {
sp := &service.Spec.Ports[i]
protocol := string(v1.ProtocolTCP)
if sp.Protocol != "" {
protocol = string(sp.Protocol)
}
hostPort := net.JoinHostPort(service.Spec.ClusterIP, strconv.Itoa(int(sp.Port)))
if i == 0 {
// Docker special-cases the first port.
all = append(all, v1.EnvVar{
Name: prefix + "_PORT",
Value: fmt.Sprintf("%s://%s", strings.ToLower(protocol), hostPort),
})
}
portPrefix := fmt.Sprintf("%s_PORT_%d_%s", prefix, sp.Port, strings.ToUpper(protocol))
all = append(all, []v1.EnvVar{
{
Name: portPrefix,
Value: fmt.Sprintf("%s://%s", strings.ToLower(protocol), hostPort),
},
{
Name: portPrefix + "_PROTO",
Value: strings.ToLower(protocol),
},
{
Name: portPrefix + "_PORT",
Value: strconv.Itoa(int(sp.Port)),
},
{
Name: portPrefix + "_ADDR",
Value: service.Spec.ClusterIP,
},
}...)
}
return all
}

156
internal/podutils/helper.go Normal file
View File

@@ -0,0 +1,156 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package podutils
import (
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
)
// ConvertDownwardAPIFieldLabel converts the specified downward API field label
// and its value in the pod of the specified version to the internal version,
// and returns the converted label and value. This function returns an error if
// the conversion fails.
func ConvertDownwardAPIFieldLabel(version, label, value string) (string, string, error) {
if version != "v1" {
return "", "", fmt.Errorf("unsupported pod version: %s", version)
}
if path, _, ok := SplitMaybeSubscriptedPath(label); ok {
switch path {
case "metadata.annotations", "metadata.labels":
return label, value, nil
default:
return "", "", fmt.Errorf("field label does not support subscript: %s", label)
}
}
switch label {
case "metadata.annotations",
"metadata.labels",
"metadata.name",
"metadata.namespace",
"metadata.uid",
"spec.nodeName",
"spec.restartPolicy",
"spec.serviceAccountName",
"spec.schedulerName",
"status.phase",
"status.hostIP",
"status.podIP",
"status.podIPs":
return label, value, nil
// This is for backwards compatibility with old v1 clients which send spec.host
case "spec.host":
return "spec.nodeName", value, nil
default:
return "", "", fmt.Errorf("field label not supported: %s", label)
}
}
// ExtractFieldPathAsString extracts the field from the given object
// and returns it as a string. The object must be a pointer to an
// API type.
func ExtractFieldPathAsString(obj interface{}, fieldPath string) (string, error) {
accessor, err := meta.Accessor(obj)
if err != nil {
return "", err
}
if path, subscript, ok := SplitMaybeSubscriptedPath(fieldPath); ok {
switch path {
case "metadata.annotations":
if errs := validation.IsQualifiedName(strings.ToLower(subscript)); len(errs) != 0 {
return "", fmt.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";"))
}
return accessor.GetAnnotations()[subscript], nil
case "metadata.labels":
if errs := validation.IsQualifiedName(subscript); len(errs) != 0 {
return "", fmt.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";"))
}
return accessor.GetLabels()[subscript], nil
default:
return "", fmt.Errorf("fieldPath %q does not support subscript", fieldPath)
}
}
switch fieldPath {
case "metadata.annotations":
return FormatMap(accessor.GetAnnotations()), nil
case "metadata.labels":
return FormatMap(accessor.GetLabels()), nil
case "metadata.name":
return accessor.GetName(), nil
case "metadata.namespace":
return accessor.GetNamespace(), nil
case "metadata.uid":
return string(accessor.GetUID()), nil
}
return "", fmt.Errorf("unsupported fieldPath: %v", fieldPath)
}
// SplitMaybeSubscriptedPath checks whether the specified fieldPath is
// subscripted, and
// - if yes, this function splits the fieldPath into path and subscript, and
// returns (path, subscript, true).
// - if no, this function returns (fieldPath, "", false).
//
// Example inputs and outputs:
// - "metadata.annotations['myKey']" --> ("metadata.annotations", "myKey", true)
// - "metadata.annotations['a[b]c']" --> ("metadata.annotations", "a[b]c", true)
// - "metadata.labels[”]" --> ("metadata.labels", "", true)
// - "metadata.labels" --> ("metadata.labels", "", false)
func SplitMaybeSubscriptedPath(fieldPath string) (string, string, bool) {
if !strings.HasSuffix(fieldPath, "']") {
return fieldPath, "", false
}
s := strings.TrimSuffix(fieldPath, "']")
parts := strings.SplitN(s, "['", 2)
if len(parts) < 2 {
return fieldPath, "", false
}
if len(parts[0]) == 0 {
return fieldPath, "", false
}
return parts[0], parts[1], true
}
// FormatMap formats map[string]string to a string.
func FormatMap(m map[string]string) (fmtStr string) {
// output with keys in sorted order to provide stable output
keys := sets.NewString()
for key := range m {
keys.Insert(key)
}
for _, key := range keys.List() {
fmtStr += fmt.Sprintf("%v=%q\n", key, m[key])
}
fmtStr = strings.TrimSuffix(fmtStr, "\n")
return
}
// IsServiceIPSet aims to check if the service's ClusterIP is set or not the objective is not to perform validation here
func IsServiceIPSet(service *corev1.Service) bool {
return service.Spec.ClusterIP != corev1.ClusterIPNone && service.Spec.ClusterIP != ""
}

View File

@@ -35,6 +35,9 @@ const (
MaxRetries = 20
)
// ShouldRetryFunc is a mechanism to have a custom retry policy
type ShouldRetryFunc func(ctx context.Context, key string, timesTried int, originallyAdded time.Time, err error) (*time.Duration, error)
// ItemHandler is a callback that handles a single key on the Queue
type ItemHandler func(ctx context.Context, key string) error
@@ -48,7 +51,7 @@ type Queue struct {
name string
handler ItemHandler
ratelimiter workqueue.RateLimiter
ratelimiter workqueue.TypedRateLimiter[any]
// items are items that are marked dirty waiting for processing.
items *list.List
// itemInQueue is a map of (string) key -> item while it is in the items list
@@ -61,6 +64,8 @@ type Queue struct {
// wakeup
wakeupCh chan struct{}
retryFunc ShouldRetryFunc
}
type queueItem struct {
@@ -83,9 +88,12 @@ func (item *queueItem) String() string {
// New creates a queue
//
// It expects to get a item rate limiter, and a friendly name which is used in logs, and
// in the internal kubernetes metrics.
func New(ratelimiter workqueue.RateLimiter, name string, handler ItemHandler) *Queue {
// It expects to get a item rate limiter, and a friendly name which is used in logs, and in the internal kubernetes
// metrics. If retryFunc is nil, the default retry function.
func New(ratelimiter workqueue.TypedRateLimiter[any], name string, handler ItemHandler, retryFunc ShouldRetryFunc) *Queue {
if retryFunc == nil {
retryFunc = DefaultRetryFunc
}
return &Queue{
clock: clock.RealClock{},
name: name,
@@ -96,6 +104,7 @@ func New(ratelimiter workqueue.RateLimiter, name string, handler ItemHandler) *Q
handler: handler,
wakeupCh: make(chan struct{}, 1),
waitForNextItemSemaphore: semaphore.NewWeighted(1),
retryFunc: retryFunc,
}
}
@@ -104,7 +113,7 @@ func (q *Queue) Enqueue(ctx context.Context, key string) {
q.lock.Lock()
defer q.lock.Unlock()
q.insert(ctx, key, true, 0)
q.insert(ctx, key, true, nil)
}
// EnqueueWithoutRateLimit enqueues the key without a rate limit
@@ -112,7 +121,7 @@ func (q *Queue) EnqueueWithoutRateLimit(ctx context.Context, key string) {
q.lock.Lock()
defer q.lock.Unlock()
q.insert(ctx, key, false, 0)
q.insert(ctx, key, false, nil)
}
// Forget forgets the key
@@ -142,9 +151,20 @@ func (q *Queue) Forget(ctx context.Context, key string) {
span.WithField(ctx, "status", "notfound")
}
func durationDeref(duration *time.Duration, def time.Duration) time.Duration {
if duration == nil {
return def
}
return *duration
}
// insert inserts a new item to be processed at time time. It will not further delay items if when is later than the
// original time the item was scheduled to be processed. If when is earlier, it will "bring it forward"
func (q *Queue) insert(ctx context.Context, key string, ratelimit bool, delay time.Duration) *queueItem {
// If ratelimit is specified, and delay is nil, then the ratelimiter's delay (return from When function) will be used
// If ratelimit is specified, and the delay is non-nil, then the delay value will be used
// If ratelimit is false, then only delay is used to schedule the work. If delay is nil, it will be considered 0.
func (q *Queue) insert(ctx context.Context, key string, ratelimit bool, delay *time.Duration) *queueItem {
ctx, span := trace.StartSpan(ctx, "insert")
defer span.End()
@@ -153,7 +173,9 @@ func (q *Queue) insert(ctx context.Context, key string, ratelimit bool, delay ti
"key": key,
"ratelimit": ratelimit,
})
if delay > 0 {
if delay == nil {
ctx = span.WithField(ctx, "delay", "nil")
} else {
ctx = span.WithField(ctx, "delay", delay.String())
}
@@ -167,7 +189,7 @@ func (q *Queue) insert(ctx context.Context, key string, ratelimit bool, delay ti
// First see if the item is already being processed
if item, ok := q.itemsBeingProcessed[key]; ok {
span.WithField(ctx, "status", "itemsBeingProcessed")
when := q.clock.Now().Add(delay)
when := q.clock.Now().Add(durationDeref(delay, 0))
// Is the item already been redirtied?
if item.redirtiedAt.IsZero() {
item.redirtiedAt = when
@@ -184,7 +206,7 @@ func (q *Queue) insert(ctx context.Context, key string, ratelimit bool, delay ti
if item, ok := q.itemsInQueue[key]; ok {
span.WithField(ctx, "status", "itemsInQueue")
qi := item.Value.(*queueItem)
when := q.clock.Now().Add(delay)
when := q.clock.Now().Add(durationDeref(delay, 0))
q.adjustPosition(qi, item, when)
return qi
}
@@ -198,15 +220,16 @@ func (q *Queue) insert(ctx context.Context, key string, ratelimit bool, delay ti
}
if ratelimit {
if delay > 0 {
panic("Non-zero delay with rate limiting not supported")
actualDelay := q.ratelimiter.When(key)
// Check if delay is overridden
if delay != nil {
actualDelay = *delay
}
ratelimitDelay := q.ratelimiter.When(key)
span.WithField(ctx, "delay", ratelimitDelay.String())
val.plannedToStartWorkAt = val.plannedToStartWorkAt.Add(ratelimitDelay)
val.delayedViaRateLimit = &ratelimitDelay
span.WithField(ctx, "delay", actualDelay.String())
val.plannedToStartWorkAt = val.plannedToStartWorkAt.Add(actualDelay)
val.delayedViaRateLimit = &actualDelay
} else {
val.plannedToStartWorkAt = val.plannedToStartWorkAt.Add(delay)
val.plannedToStartWorkAt = val.plannedToStartWorkAt.Add(durationDeref(delay, 0))
}
for item := q.items.Back(); item != nil; item = item.Prev() {
@@ -244,7 +267,7 @@ func (q *Queue) adjustPosition(qi *queueItem, element *list.Element, when time.T
func (q *Queue) EnqueueWithoutRateLimitWithDelay(ctx context.Context, key string, after time.Duration) {
q.lock.Lock()
defer q.lock.Unlock()
q.insert(ctx, key, false, after)
q.insert(ctx, key, false, &after)
}
// Empty returns if the queue has no items in it
@@ -265,6 +288,24 @@ func (q *Queue) Len() int {
return q.items.Len() + len(q.itemsBeingProcessed)
}
// UnprocessedLen returns the count of items yet to be processed in the queue
func (q *Queue) UnprocessedLen() int {
q.lock.Lock()
defer q.lock.Unlock()
if q.items.Len() != len(q.itemsInQueue) {
panic("Internally inconsistent state")
}
return len(q.itemsInQueue)
}
// ProcessedLen returns the count items that are being processed
func (q *Queue) ItemsBeingProcessedLen() int {
q.lock.Lock()
defer q.lock.Unlock()
return len(q.itemsBeingProcessed)
}
// Run starts the workers
//
// It blocks until context is cancelled, and all of the workers exit.
@@ -423,25 +464,37 @@ func (q *Queue) handleQueueItemObject(ctx context.Context, qi *queueItem) error
}
if err != nil {
if qi.requeues+1 < MaxRetries {
ctx = span.WithField(ctx, "error", err.Error())
var delay *time.Duration
// Stash the original error for logging below
originalError := err
delay, err = q.retryFunc(ctx, qi.key, qi.requeues+1, qi.originallyAdded, err)
if err == nil {
// Put the item back on the work Queue to handle any transient errors.
log.G(ctx).WithError(err).Warnf("requeuing %q due to failed sync", qi.key)
newQI := q.insert(ctx, qi.key, true, 0)
log.G(ctx).WithError(originalError).Warnf("requeuing %q due to failed sync", qi.key)
newQI := q.insert(ctx, qi.key, true, delay)
newQI.requeues = qi.requeues + 1
newQI.originallyAdded = qi.originallyAdded
return nil
}
err = pkgerrors.Wrapf(err, "forgetting %q due to maximum retries reached", qi.key)
if !qi.redirtiedAt.IsZero() {
err = fmt.Errorf("temporarily (requeued) forgetting %q due to: %w", qi.key, err)
} else {
err = fmt.Errorf("forgetting %q due to: %w", qi.key, err)
}
}
// We've exceeded the maximum retries or we were successful.
q.ratelimiter.Forget(qi.key)
if !qi.redirtiedAt.IsZero() {
newQI := q.insert(ctx, qi.key, qi.redirtiedWithRatelimit, time.Until(qi.redirtiedAt))
delay := time.Until(qi.redirtiedAt)
newQI := q.insert(ctx, qi.key, qi.redirtiedWithRatelimit, &delay)
newQI.addedViaRedirty = true
}
span.SetStatus(err)
return err
}
@@ -456,3 +509,12 @@ func (q *Queue) String() string {
}
return fmt.Sprintf("<items:%s>", items)
}
// DefaultRetryFunc is the default function used for retries by the queue subsystem.
func DefaultRetryFunc(ctx context.Context, key string, timesTried int, originallyAdded time.Time, err error) (*time.Duration, error) {
if timesTried < MaxRetries {
return nil, nil
}
return nil, pkgerrors.Wrapf(err, "maximum retries (%d) reached", MaxRetries)
}

View File

@@ -19,6 +19,10 @@ import (
"k8s.io/utils/clock"
)
func durationPtr(d time.Duration) *time.Duration {
return &d
}
func TestQueueMaxRetries(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -31,11 +35,11 @@ func TestQueueMaxRetries(t *testing.T) {
n++
return knownErr
}
wq := New(workqueue.NewMaxOfRateLimiter(
wq := New(workqueue.NewTypedMaxOfRateLimiter[any](
// The default upper bound is 1000 seconds. Let's not use that.
workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 10*time.Millisecond),
&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
), t.Name(), handler)
workqueue.NewTypedItemExponentialFailureRateLimiter[any](5*time.Millisecond, 10*time.Millisecond),
&workqueue.TypedBucketRateLimiter[any]{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
), t.Name(), handler, nil)
wq.Enqueue(context.TODO(), "test")
for n < MaxRetries {
@@ -46,12 +50,63 @@ func TestQueueMaxRetries(t *testing.T) {
assert.Assert(t, is.Equal(0, wq.Len()))
}
func TestQueueCustomRetries(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
logger := logrus.New()
logger.SetLevel(logrus.DebugLevel)
ctx = log.WithLogger(ctx, logruslogger.FromLogrus(logrus.NewEntry(logger)))
n := 0
errorSeen := 0
retryTestError := errors.New("Error should be retried every 10 milliseconds")
handler := func(ctx context.Context, key string) error {
if key == "retrytest" {
n++
return retryTestError
}
return errors.New("Unknown error")
}
shouldRetryFunc := func(ctx context.Context, key string, timesTried int, originallyAdded time.Time, err error) (*time.Duration, error) {
var sleepTime *time.Duration
if errors.Is(err, retryTestError) {
errorSeen++
sleepTime = durationPtr(10 * time.Millisecond)
}
_, retErr := DefaultRetryFunc(ctx, key, timesTried, originallyAdded, err)
return sleepTime, retErr
}
wq := New(&workqueue.TypedBucketRateLimiter[any]{Limiter: rate.NewLimiter(rate.Limit(1000), 1000)}, t.Name(), handler, shouldRetryFunc)
timeTaken := func(key string) time.Duration {
start := time.Now()
wq.Enqueue(context.TODO(), key)
for i := 0; i < MaxRetries; i++ {
assert.Assert(t, wq.handleQueueItem(ctx))
}
return time.Since(start)
}
unknownTime := timeTaken("unknown")
assert.Assert(t, n == 0)
assert.Assert(t, unknownTime < 10*time.Millisecond)
retrytestTime := timeTaken("retrytest")
assert.Assert(t, is.Equal(n, MaxRetries))
assert.Assert(t, is.Equal(errorSeen, MaxRetries))
assert.Assert(t, is.Equal(0, wq.Len()))
assert.Assert(t, retrytestTime > 10*time.Millisecond*time.Duration(n-1))
assert.Assert(t, retrytestTime < 2*10*time.Millisecond*time.Duration(n-1))
}
func TestForget(t *testing.T) {
t.Parallel()
handler := func(ctx context.Context, key string) error {
panic("Should never be called")
}
wq := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), handler)
wq := New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), handler, nil)
wq.Forget(context.TODO(), "val")
assert.Assert(t, is.Equal(0, wq.Len()))
@@ -66,9 +121,9 @@ func TestQueueEmpty(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
q := New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), func(ctx context.Context, key string) error {
return nil
})
}, nil)
item, err := q.getNextItem(ctx)
assert.Error(t, err, context.DeadlineExceeded.Error())
@@ -81,13 +136,13 @@ func TestQueueItemNoSleep(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Millisecond)
defer cancel()
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
q := New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), func(ctx context.Context, key string) error {
return nil
})
}, nil)
q.lock.Lock()
q.insert(ctx, "foo", false, -1*time.Hour)
q.insert(ctx, "bar", false, -1*time.Hour)
q.insert(ctx, "foo", false, durationPtr(-1*time.Hour))
q.insert(ctx, "bar", false, durationPtr(-1*time.Hour))
q.lock.Unlock()
item, err := q.getNextItem(ctx)
@@ -105,12 +160,12 @@ func TestQueueItemSleep(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Millisecond)
defer cancel()
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
q := New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), func(ctx context.Context, key string) error {
return nil
})
}, nil)
q.lock.Lock()
q.insert(ctx, "foo", false, 100*time.Millisecond)
q.insert(ctx, "bar", false, 100*time.Millisecond)
q.insert(ctx, "foo", false, durationPtr(100*time.Millisecond))
q.insert(ctx, "bar", false, durationPtr(100*time.Millisecond))
q.lock.Unlock()
item, err := q.getNextItem(ctx)
@@ -124,14 +179,14 @@ func TestQueueBackgroundAdd(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5000*time.Millisecond)
defer cancel()
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
q := New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), func(ctx context.Context, key string) error {
return nil
})
}, nil)
start := time.Now()
time.AfterFunc(100*time.Millisecond, func() {
q.lock.Lock()
defer q.lock.Unlock()
q.insert(ctx, "foo", false, 0)
q.insert(ctx, "foo", false, nil)
})
item, err := q.getNextItem(ctx)
@@ -146,18 +201,18 @@ func TestQueueBackgroundAdvance(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5000*time.Millisecond)
defer cancel()
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
q := New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), func(ctx context.Context, key string) error {
return nil
})
}, nil)
start := time.Now()
q.lock.Lock()
q.insert(ctx, "foo", false, 10*time.Second)
q.insert(ctx, "foo", false, durationPtr(10*time.Second))
q.lock.Unlock()
time.AfterFunc(200*time.Millisecond, func() {
q.lock.Lock()
defer q.lock.Unlock()
q.insert(ctx, "foo", false, 0)
q.insert(ctx, "foo", false, nil)
})
item, err := q.getNextItem(ctx)
@@ -175,7 +230,7 @@ func TestQueueRedirty(t *testing.T) {
var times int64
var q *Queue
q = New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
q = New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), func(ctx context.Context, key string) error {
assert.Assert(t, is.Equal(key, "foo"))
if atomic.AddInt64(&times, 1) == 1 {
q.EnqueueWithoutRateLimit(context.TODO(), "foo")
@@ -183,7 +238,7 @@ func TestQueueRedirty(t *testing.T) {
cancel()
}
return nil
})
}, nil)
q.EnqueueWithoutRateLimit(context.TODO(), "foo")
q.Run(ctx, 1)
@@ -201,11 +256,11 @@ func TestHeapConcurrency(t *testing.T) {
start := time.Now()
seen := sync.Map{}
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
q := New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), func(ctx context.Context, key string) error {
seen.Store(key, struct{}{})
time.Sleep(time.Second)
return nil
})
}, nil)
for i := 0; i < 20; i++ {
q.EnqueueWithoutRateLimit(context.TODO(), strconv.Itoa(i))
}
@@ -236,9 +291,9 @@ func checkConsistency(t *testing.T, q *Queue) {
}
func TestHeapOrder(t *testing.T) {
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
q := New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), func(ctx context.Context, key string) error {
return nil
})
}, nil)
q.clock = nonmovingClock{}
q.EnqueueWithoutRateLimitWithDelay(context.TODO(), "a", 1000)
@@ -262,7 +317,7 @@ func TestHeapOrder(t *testing.T) {
type rateLimitWrapper struct {
addedMap sync.Map
forgottenMap sync.Map
rl workqueue.RateLimiter
rl workqueue.TypedRateLimiter[any]
}
func (r *rateLimitWrapper) When(item interface{}) time.Duration {
@@ -301,7 +356,7 @@ func TestRateLimiter(t *testing.T) {
start := time.Now()
ratelimiter := &rateLimitWrapper{
rl: workqueue.NewItemFastSlowRateLimiter(1*time.Millisecond, 100*time.Millisecond, 1),
rl: workqueue.NewTypedItemFastSlowRateLimiter[any](1*time.Millisecond, 100*time.Millisecond, 1),
}
q := New(ratelimiter, t.Name(), func(ctx context.Context, key string) error {
@@ -311,7 +366,7 @@ func TestRateLimiter(t *testing.T) {
return errors.New("test")
}
return nil
})
}, nil)
enqueued := 0
syncMap.Range(func(key, value interface{}) bool {
@@ -366,12 +421,12 @@ func TestQueueForgetInProgress(t *testing.T) {
var times int64
var q *Queue
q = New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
q = New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), func(ctx context.Context, key string) error {
assert.Assert(t, is.Equal(key, "foo"))
atomic.AddInt64(&times, 1)
q.Forget(context.TODO(), key)
return errors.New("test")
})
}, nil)
q.EnqueueWithoutRateLimit(context.TODO(), "foo")
go q.Run(ctx, 1)
@@ -386,9 +441,9 @@ func TestQueueForgetBeforeStart(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
q := New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), func(ctx context.Context, key string) error {
panic("shouldn't be called")
})
}, nil)
q.EnqueueWithoutRateLimit(context.TODO(), "foo")
q.Forget(context.TODO(), "foo")
@@ -403,26 +458,26 @@ func TestQueueMoveItem(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
q := New(workqueue.DefaultTypedItemBasedRateLimiter[any](), t.Name(), func(ctx context.Context, key string) error {
panic("shouldn't be called")
})
}, nil)
q.clock = nonmovingClock{}
q.insert(ctx, "foo", false, 3000)
q.insert(ctx, "bar", false, 2000)
q.insert(ctx, "baz", false, 1000)
q.insert(ctx, "foo", false, durationPtr(3000))
q.insert(ctx, "bar", false, durationPtr(2000))
q.insert(ctx, "baz", false, durationPtr(1000))
checkConsistency(t, q)
t.Log(q)
q.insert(ctx, "foo", false, 2000)
q.insert(ctx, "foo", false, durationPtr(2000))
checkConsistency(t, q)
t.Log(q)
q.insert(ctx, "foo", false, 1999)
q.insert(ctx, "foo", false, durationPtr(1999))
checkConsistency(t, q)
t.Log(q)
q.insert(ctx, "foo", false, 999)
q.insert(ctx, "foo", false, durationPtr(999))
checkConsistency(t, q)
t.Log(q)
}

View File

@@ -12,7 +12,6 @@ import (
watchapi "k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/watch"
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
)
// CreateDummyPodObjectWithPrefix creates a dujmmy pod object using the specified prefix as the value of .metadata.generateName.
@@ -101,15 +100,25 @@ func (f *Framework) WaitUntilPodCondition(namespace, name string, fn watch.Condi
func (f *Framework) WaitUntilPodReady(namespace, name string) (*corev1.Pod, error) {
return f.WaitUntilPodCondition(namespace, name, func(event watchapi.Event) (bool, error) {
pod := event.Object.(*corev1.Pod)
return pod.Status.Phase == corev1.PodRunning && podutil.IsPodReady(pod) && pod.Status.PodIP != "", nil
return pod.Status.Phase == corev1.PodRunning && IsPodReady(pod) && pod.Status.PodIP != "", nil
})
}
// IsPodReady returns true if a pod is ready.
func IsPodReady(pod *corev1.Pod) bool {
for _, cond := range pod.Status.Conditions {
if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
return true
}
}
return false
}
// WaitUntilPodDeleted blocks until the pod with the specified name and namespace is deleted from apiserver.
func (f *Framework) WaitUntilPodDeleted(namespace, name string) (*corev1.Pod, error) {
return f.WaitUntilPodCondition(namespace, name, func(event watchapi.Event) (bool, error) {
pod := event.Object.(*corev1.Pod)
return event.Type == watchapi.Deleted || pod.ObjectMeta.DeletionTimestamp != nil, nil
return event.Type == watchapi.Deleted || pod.DeletionTimestamp != nil, nil
})
}

View File

@@ -3,10 +3,10 @@ package framework
import (
"context"
"encoding/json"
"strconv"
api "github.com/virtual-kubelet/virtual-kubelet/node/api"
"k8s.io/apimachinery/pkg/util/net"
stats "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
)
// GetStatsSummary queries the /stats/summary endpoint of the virtual-kubelet and returns the Summary object obtained as a response.
@@ -18,7 +18,7 @@ func (f *Framework) GetStatsSummary(ctx context.Context) (*stats.Summary, error)
Namespace(f.Namespace).
Resource("pods").
SubResource("proxy").
Name(net.JoinSchemeNamePort("http", f.NodeName, strconv.Itoa(10255))).
Name(net.JoinSchemeNamePort("https", f.NodeName, "10250")).
Suffix("/stats/summary").DoRaw(ctx)
if err != nil {
return nil, err
@@ -30,3 +30,21 @@ func (f *Framework) GetStatsSummary(ctx context.Context) (*stats.Summary, error)
}
return res, nil
}
// GetStatsSummary queries the /metrics/resource endpoint of the virtual-kubelet and returns the Summary object obtained as a response.
func (f *Framework) GetMetricsResource(ctx context.Context) ([]byte, error) {
// Query the /stats/summary endpoint.
b, err := f.KubeClient.CoreV1().
RESTClient().
Get().
Namespace(f.Namespace).
Resource("pods").
SubResource("proxy").
Name(net.JoinSchemeNamePort("https", f.NodeName, "10250")).
Suffix(api.MetricsResourceRouteSuffix).DoRaw(ctx)
if err != nil {
return nil, err
}
return b, nil
}

View File

@@ -1,3 +1,4 @@
//go:build e2e
// +build e2e
package e2e
@@ -48,7 +49,7 @@ func teardown() error {
return nil
}
// Provider-specific shouldSkipTest function
// Provider-specific shouldSkipTest function
func shouldSkipTest(testName string) bool {
return false
}

View File

@@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,

109
log/slog/slog.go Normal file
View File

@@ -0,0 +1,109 @@
// Copyright © 2021 The virtual-kubelet authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Package slog implements a virtual-kubelet/log.Logger using slog as a backend
// You can use this by creating a slog logger and calling `FromSlog(logger)`
// If you want this to be the default logger for virtual-kubelet, set `log.L` to the value returned by `FromSlog(logger)`
package slog
import (
"context"
"fmt"
"log/slog"
"github.com/virtual-kubelet/virtual-kubelet/log"
)
// Ensure log.Logger is fully implemented during compile time.
var _ log.Logger = (*adapter)(nil)
// Create a custom logging level for the Fatal level as slog does not
// have this level by default
const LevelFatal = slog.Level(12)
// adapter implements the `log.Logger` interface for slog
type adapter struct {
inner *slog.Logger
}
// FromSlog creates a new `log.Logger` from a slog logger
func FromSlog(logger *slog.Logger) log.Logger {
return &adapter{inner: logger}
}
func (l *adapter) Debug(args ...interface{}) {
msg := args[0].(string)
l.inner.Debug(msg)
}
func (l *adapter) Debugf(format string, args ...interface{}) {
formattedArgs := fmt.Sprintf(format, args...)
l.inner.Debug(formattedArgs)
}
func (l *adapter) Info(args ...interface{}) {
msg := args[0].(string)
l.inner.Info(msg)
}
func (l *adapter) Infof(format string, args ...interface{}) {
formattedArgs := fmt.Sprintf(format, args...)
l.inner.Info(formattedArgs)
}
func (l *adapter) Warn(args ...interface{}) {
msg := args[0].(string)
l.inner.Warn(msg)
}
func (l *adapter) Warnf(format string, args ...interface{}) {
formattedArgs := fmt.Sprintf(format, args...)
l.inner.Warn(formattedArgs)
}
func (l *adapter) Error(args ...interface{}) {
msg := args[0].(string)
l.inner.Error(msg)
}
func (l *adapter) Errorf(format string, args ...interface{}) {
formattedArgs := fmt.Sprintf(format, args...)
l.inner.Error(formattedArgs)
}
func (l *adapter) Fatal(args ...interface{}) {
msg := args[0].(string)
l.inner.Log(context.Background(), LevelFatal, msg)
}
func (l *adapter) Fatalf(format string, args ...interface{}) {
formattedArgs := fmt.Sprintf(format, args...)
l.inner.Log(context.Background(), LevelFatal, formattedArgs)
}
func (l *adapter) WithField(key string, val interface{}) log.Logger {
return &adapter{inner: l.inner.With(key, val)}
}
func (l *adapter) WithFields(f log.Fields) log.Logger {
logger := l.inner
for k, v := range f {
logger = logger.With(k, v)
}
return &adapter{inner: logger}
}
func (l *adapter) WithError(err error) log.Logger {
return &adapter{inner: l.inner.With("error", err)}
}

148
node/api/attach.go Normal file
View File

@@ -0,0 +1,148 @@
// Copyright © 2017 The virtual-kubelet authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"context"
"io"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/virtual-kubelet/virtual-kubelet/errdefs"
"github.com/virtual-kubelet/virtual-kubelet/internal/kubernetes/remotecommand"
"k8s.io/apimachinery/pkg/types"
remoteutils "k8s.io/client-go/tools/remotecommand"
)
// ContainerAttachHandlerFunc defines the handler function used for "execing" into a
// container in a pod.
type ContainerAttachHandlerFunc func(ctx context.Context, namespace, podName, containerName string, attach AttachIO) error
// HandleContainerAttach makes an http handler func from a Provider which execs a command in a pod's container
// Note that this handler currently depends on gorrilla/mux to get url parts as variables.
// TODO(@cpuguy83): don't force gorilla/mux on consumers of this function
func HandleContainerAttach(h ContainerAttachHandlerFunc, opts ...ContainerExecHandlerOption) http.HandlerFunc {
if h == nil {
return NotImplemented
}
var cfg ContainerExecHandlerConfig
for _, o := range opts {
o(&cfg)
}
if cfg.StreamIdleTimeout == 0 {
cfg.StreamIdleTimeout = 30 * time.Second
}
if cfg.StreamCreationTimeout == 0 {
cfg.StreamCreationTimeout = 30 * time.Second
}
return handleError(func(w http.ResponseWriter, req *http.Request) error {
vars := mux.Vars(req)
namespace := vars["namespace"]
pod := vars["pod"]
container := vars["container"]
supportedStreamProtocols := strings.Split(req.Header.Get("X-Stream-Protocol-Version"), ",")
streamOpts, err := getExecOptions(req)
if err != nil {
return errdefs.AsInvalidInput(err)
}
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
attach := &containerAttachContext{ctx: ctx, h: h, pod: pod, namespace: namespace, container: container}
remotecommand.ServeAttach(
w,
req,
attach,
"",
"",
container,
streamOpts,
cfg.StreamIdleTimeout,
cfg.StreamCreationTimeout,
supportedStreamProtocols,
)
return nil
})
}
type containerAttachContext struct {
h ContainerAttachHandlerFunc
namespace, pod, container string
ctx context.Context
}
// AttachToContainer Implements remotecommand.Attacher
// This is called by remotecommand.ServeAttach
func (c *containerAttachContext) AttachToContainer(
name string,
uid types.UID,
container string,
in io.Reader,
out, err io.WriteCloser,
tty bool,
resize <-chan remoteutils.TerminalSize,
timeout time.Duration,
) error {
eio := &execIO{
tty: tty,
stdin: in,
stdout: out,
stderr: err,
}
if tty {
eio.chResize = make(chan TermSize)
}
ctx, cancel := context.WithCancel(c.ctx)
defer cancel()
if tty {
go func() {
send := func(s remoteutils.TerminalSize) bool {
select {
case eio.chResize <- TermSize{Width: s.Width, Height: s.Height}:
return false
case <-ctx.Done():
return true
}
}
for {
select {
case s := <-resize:
if send(s) {
return
}
case <-ctx.Done():
return
}
}
}()
}
return c.h(c.ctx, c.namespace, c.pod, c.container, eio)
}

View File

@@ -27,7 +27,6 @@ import (
"github.com/virtual-kubelet/virtual-kubelet/internal/kubernetes/remotecommand"
"k8s.io/apimachinery/pkg/types"
remoteutils "k8s.io/client-go/tools/remotecommand"
api "k8s.io/kubernetes/pkg/apis/core"
)
// ContainerExecHandlerFunc defines the handler function used for "execing" into a
@@ -136,11 +135,18 @@ func HandleContainerExec(h ContainerExecHandlerFunc, opts ...ContainerExecHandle
})
}
const (
execTTYParam = "tty"
execStdinParam = "input"
execStdoutParam = "output"
execStderrParam = "error"
)
func getExecOptions(req *http.Request) (*remotecommand.Options, error) {
tty := req.FormValue(api.ExecTTYParam) == "1"
stdin := req.FormValue(api.ExecStdinParam) == "1"
stdout := req.FormValue(api.ExecStdoutParam) == "1"
stderr := req.FormValue(api.ExecStderrParam) == "1"
tty := req.FormValue(execTTYParam) == "1"
stdin := req.FormValue(execStdinParam) == "1"
stdout := req.FormValue(execStdoutParam) == "1"
stderr := req.FormValue(execStderrParam) == "1"
if tty && stderr {
return nil, errors.New("cannot exec with tty and stderr")
}
@@ -165,7 +171,17 @@ type containerExecContext struct {
// ExecInContainer Implements remotecommand.Executor
// This is called by remotecommand.ServeExec
func (c *containerExecContext) ExecInContainer(name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remoteutils.TerminalSize, timeout time.Duration) error {
func (c *containerExecContext) ExecInContainer(
name string,
uid types.UID,
container string,
cmd []string,
in io.Reader,
out, err io.WriteCloser,
tty bool,
resize <-chan remoteutils.TerminalSize,
timeout time.Duration,
) error {
eio := &execIO{
tty: tty,

View File

@@ -33,7 +33,9 @@ func handleError(f handlerFunc) http.HandlerFunc {
code := httpStatusCode(err)
w.WriteHeader(code)
io.WriteString(w, err.Error()) //nolint:errcheck
if _, err := io.WriteString(w, err.Error()); err != nil {
log.G(req.Context()).WithError(err).Error("error writing error response")
}
logger := log.G(req.Context()).WithError(err).WithField("httpStatusCode", code)
if code >= 500 {

67
node/api/metrics.go Normal file
View File

@@ -0,0 +1,67 @@
// Copyright © 2017 The virtual-kubelet authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"bytes"
"context"
"net/http"
"github.com/pkg/errors"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
)
const (
PrometheusTextFormatContentType = "text/plain; version=0.0.4"
)
// PodMetricsResourceHandlerFunc defines the handler for getting pod metrics
type PodMetricsResourceHandlerFunc func(context.Context) ([]*dto.MetricFamily, error)
// HandlePodMetricsResource makes an HTTP handler for implementing the kubelet /metrics/resource endpoint
func HandlePodMetricsResource(h PodMetricsResourceHandlerFunc) http.HandlerFunc {
if h == nil {
return NotImplemented
}
return handleError(func(w http.ResponseWriter, req *http.Request) error {
metrics, err := h(req.Context())
if err != nil {
if isCancelled(err) {
return err
}
return errors.Wrap(err, "error getting status from provider")
}
// Convert metrics to Prometheus text format.
var buffer bytes.Buffer
enc := expfmt.NewEncoder(&buffer, expfmt.NewFormat(expfmt.TypeTextPlain))
for _, mf := range metrics {
if err := enc.Encode(mf); err != nil {
return errors.Wrap(err, "could not convert metrics to prometheus text format")
}
}
// Set the response content type to "text/plain; version=0.0.4".
w.Header().Set("Content-Type", PrometheusTextFormatContentType)
// Write the metrics in Prometheus text format to the response writer.
if _, err := w.Write(buffer.Bytes()); err != nil {
return errors.Wrap(err, "could not write to client")
}
return nil
})
}

149
node/api/metrics_test.go Normal file
View File

@@ -0,0 +1,149 @@
// Copyright © 2017 The virtual-kubelet authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api_test
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/pkg/errors"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/virtual-kubelet/virtual-kubelet/node/api"
"google.golang.org/protobuf/proto"
)
const (
prometheusContentType = "text/plain; version=0.0.4"
)
func TestHandlePodMetricsResource(t *testing.T) {
testCases := []struct {
name string
handler api.PodMetricsResourceHandlerFunc
expectedStatusCode int
expectedError error
}{
{
name: "Valid PodMetricsResourceHandlerFunc",
handler: func(_ context.Context) ([]*dto.MetricFamily, error) {
// Create the expected metrics.
cpuUsageMetric := &dto.MetricFamily{
Name: proto.String("container_cpu_usage_seconds_total"),
Help: proto.String("[ALPHA] Cumulative cpu time consumed by the container in core-seconds"),
Type: dto.MetricType_GAUGE.Enum(),
Metric: []*dto.Metric{},
}
memoryUsageMetric := &dto.MetricFamily{
Name: proto.String("container_memory_working_set_bytes"),
Help: proto.String("[ALPHA] Current working set of the container in bytes"),
Type: dto.MetricType_GAUGE.Enum(),
Metric: []*dto.Metric{},
}
// Add the sample metrics to the metric families.
cpuUsageMetric.Metric = append(cpuUsageMetric.Metric, createSampleMetric(
map[string]string{
"container": "simple-hello-world-container",
"namespace": "k8se-apps",
"pod": "test-pod--zruwatj-86454fdc54-2wwpw",
},
0.1104636, 1680536423102,
))
cpuUsageMetric.Metric = append(cpuUsageMetric.Metric, createSampleMetric(
map[string]string{
"container": "simple-hello-world-container",
"namespace": "k8se-apps",
"pod": "test-pod--zruwatj-86454fdc54-4mzd4",
},
0.11322, 1680536423103,
))
memoryUsageMetric.Metric = append(memoryUsageMetric.Metric, createSampleMetric(
map[string]string{
"container": "simple-hello-world-container",
"namespace": "k8se-apps",
"pod": "test-pod--zruwatj-86454fdc54-2wwpw",
},
2.3277568e+07, 1680536423102,
))
memoryUsageMetric.Metric = append(memoryUsageMetric.Metric, createSampleMetric(
map[string]string{
"container": "simple-hello-world-container",
"namespace": "k8se-apps",
"pod": "test-pod--zruwatj-86454fdc54-4mzd4",
},
2.2450176e+07, 1680536423104,
))
return []*dto.MetricFamily{cpuUsageMetric, memoryUsageMetric}, nil
},
expectedStatusCode: http.StatusOK,
},
{
name: "Nil PodMetricsResourceHandlerFunc",
handler: nil,
expectedStatusCode: http.StatusNotImplemented,
},
{
name: "Error in PodMetricsResourceHandlerFunc",
handler: func(_ context.Context) ([]*dto.MetricFamily, error) {
return nil, errors.New("test error")
},
expectedStatusCode: http.StatusInternalServerError,
expectedError: errors.New("error getting status from provider: test error"),
},
// Add more test cases as needed
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
h := api.HandlePodMetricsResource(tc.handler)
require.NotNil(t, h)
rr := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/metrics/resource", nil)
require.NoError(t, err)
h.ServeHTTP(rr, req)
assert.Equal(t, tc.expectedStatusCode, rr.Code)
if tc.expectedError != nil {
bodyBytes, err := io.ReadAll(rr.Body)
require.NoError(t, err)
assert.Contains(t, string(bodyBytes), tc.expectedError.Error())
} else if tc.expectedStatusCode == http.StatusOK {
contentType := rr.Header().Get("Content-Type")
assert.Equal(t, prometheusContentType, contentType)
}
})
}
}
func createSampleMetric(labels map[string]string, value float64, timestamp int64) *dto.Metric {
labelPairs := []*dto.LabelPair{}
for k, v := range labels {
labelPairs = append(labelPairs, &dto.LabelPair{
Name: proto.String(k),
Value: proto.String(v),
})
}
return &dto.Metric{Label: labelPairs, Gauge: &dto.Gauge{Value: &value}, TimestampMs: &timestamp}
}

View File

@@ -24,14 +24,15 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer"
)
type PodListerFunc func(context.Context) ([]*v1.Pod, error) // nolint:golint
type PodListerFunc func(context.Context) ([]*v1.Pod, error)
func HandleRunningPods(getPods PodListerFunc) http.HandlerFunc { // nolint:golint
func HandleRunningPods(getPods PodListerFunc) http.HandlerFunc {
if getPods == nil {
return NotImplemented
}
scheme := runtime.NewScheme()
/* #nosec */
v1.SchemeBuilder.AddToScheme(scheme) //nolint:errcheck
codecs := serializer.NewCodecFactory(scheme)

116
node/api/portforward.go Normal file
View File

@@ -0,0 +1,116 @@
// Copyright © 2017 The virtual-kubelet authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"context"
"io"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/virtual-kubelet/virtual-kubelet/internal/kubernetes/portforward"
"k8s.io/apimachinery/pkg/types"
)
// PortForwardHandlerFunc defines the handler function used to
// portforward, passing through the original dataStream
type PortForwardHandlerFunc func(ctx context.Context, namespace, pod string, port int32, stream io.ReadWriteCloser) error
// PortForwardHandlerConfig is used to pass options to options to the container exec handler.
type PortForwardHandlerConfig struct {
// StreamIdleTimeout is the maximum time a streaming connection
// can be idle before the connection is automatically closed.
StreamIdleTimeout time.Duration
// StreamCreationTimeout is the maximum time for streaming connection
StreamCreationTimeout time.Duration
}
// PortForwardHandlerOption configures a PortForwardHandlerConfig
// It is used as functional options passed to `HandlePortForward`
type PortForwardHandlerOption func(*PortForwardHandlerConfig)
// WithPortForwardStreamIdleTimeout sets the idle timeout for a container port forward streaming
func WithPortForwardStreamIdleTimeout(dur time.Duration) PortForwardHandlerOption {
return func(cfg *PortForwardHandlerConfig) {
cfg.StreamIdleTimeout = dur
}
}
// WithPortForwardCreationTimeout sets the creation timeout for a container exec stream
func WithPortForwardCreationTimeout(dur time.Duration) PortForwardHandlerOption {
return func(cfg *PortForwardHandlerConfig) {
cfg.StreamCreationTimeout = dur
}
}
// HandlePortForward makes an http handler func from a Provider which forward ports to a container
// Note that this handler currently depends on gorrilla/mux to get url parts as variables.
func HandlePortForward(h PortForwardHandlerFunc, opts ...PortForwardHandlerOption) http.HandlerFunc {
if h == nil {
return NotImplemented
}
var cfg PortForwardHandlerConfig
for _, o := range opts {
o(&cfg)
}
if cfg.StreamIdleTimeout == 0 {
cfg.StreamIdleTimeout = 30 * time.Second
}
if cfg.StreamCreationTimeout == 0 {
cfg.StreamCreationTimeout = 30 * time.Second
}
return handleError(func(w http.ResponseWriter, req *http.Request) error {
vars := mux.Vars(req)
namespace := vars["namespace"]
pod := vars["pod"]
supportedStreamProtocols := strings.Split(req.Header.Get("X-Stream-Protocol-Version"), ",")
portfwd := &portForwardContext{h: h, pod: pod, namespace: namespace}
portforward.ServePortForward(
w,
req,
portfwd,
pod,
"",
&portforward.V4Options{}, // This is only used for websocket connection
cfg.StreamIdleTimeout,
cfg.StreamCreationTimeout,
supportedStreamProtocols,
)
return nil
})
}
type portForwardContext struct {
h PortForwardHandlerFunc
pod string
namespace string
}
// PortForward Implements portforward.Portforwarder
// This is called by portforward.ServePortForward
func (p *portForwardContext) PortForward(ctx context.Context, name string, uid types.UID, port int32, stream io.ReadWriteCloser) error {
return p.h(ctx, p.namespace, p.pod, port, stream)
}

View File

@@ -33,18 +33,23 @@ type ServeMux interface {
Handle(path string, h http.Handler)
}
type PodHandlerConfig struct { // nolint:golint
RunInContainer ContainerExecHandlerFunc
GetContainerLogs ContainerLogsHandlerFunc
type PodHandlerConfig struct { //nolint:golint
RunInContainer ContainerExecHandlerFunc
AttachToContainer ContainerAttachHandlerFunc
PortForward PortForwardHandlerFunc
GetContainerLogs ContainerLogsHandlerFunc
// GetPods is meant to enumerate the pods that the provider knows about
GetPods PodListerFunc
// GetPodsFromKubernetes is meant to enumerate the pods that the node is meant to be running
GetPodsFromKubernetes PodListerFunc
GetStatsSummary PodStatsSummaryHandlerFunc
GetMetricsResource PodMetricsResourceHandlerFunc
StreamIdleTimeout time.Duration
StreamCreationTimeout time.Duration
}
const MetricsResourceRouteSuffix = "/metrics/resource"
// PodHandler creates an http handler for interacting with pods/containers.
func PodHandler(p PodHandlerConfig, debug bool) http.Handler {
r := mux.NewRouter()
@@ -54,7 +59,6 @@ func PodHandler(p PodHandlerConfig, debug bool) http.Handler {
if debug {
r.HandleFunc("/runningpods/", HandleRunningPods(p.GetPods)).Methods("GET")
}
r.HandleFunc("/pods", HandleRunningPods(p.GetPodsFromKubernetes)).Methods("GET")
r.HandleFunc("/containerLogs/{namespace}/{pod}/{container}", HandleContainerLogs(p.GetContainerLogs)).Methods("GET")
r.HandleFunc(
@@ -65,6 +69,22 @@ func PodHandler(p PodHandlerConfig, debug bool) http.Handler {
WithExecStreamIdleTimeout(p.StreamIdleTimeout),
),
).Methods("POST", "GET")
r.HandleFunc(
"/attach/{namespace}/{pod}/{container}",
HandleContainerAttach(
p.AttachToContainer,
WithExecStreamCreationTimeout(p.StreamCreationTimeout),
WithExecStreamIdleTimeout(p.StreamIdleTimeout),
),
).Methods("POST", "GET")
r.HandleFunc(
"/portForward/{namespace}/{pod}",
HandlePortForward(
p.PortForward,
WithPortForwardStreamIdleTimeout(p.StreamCreationTimeout),
WithPortForwardCreationTimeout(p.StreamIdleTimeout),
),
).Methods("POST", "GET")
if p.GetStatsSummary != nil {
f := HandlePodStatsSummary(p.GetStatsSummary)
@@ -72,6 +92,11 @@ func PodHandler(p PodHandlerConfig, debug bool) http.Handler {
r.HandleFunc("/stats/summary/", f).Methods("GET")
}
if p.GetMetricsResource != nil {
f := HandlePodMetricsResource(p.GetMetricsResource)
r.HandleFunc(MetricsResourceRouteSuffix, f).Methods("GET")
r.HandleFunc(MetricsResourceRouteSuffix+"/", f).Methods("GET")
}
r.NotFoundHandler = http.HandlerFunc(NotFound)
return r
}
@@ -79,7 +104,7 @@ func PodHandler(p PodHandlerConfig, debug bool) http.Handler {
// PodStatsSummaryHandler creates an http handler for serving pod metrics.
//
// If the passed in handler func is nil this will create handlers which only
// serves http.StatusNotImplemented
// serves http.StatusNotImplemented
func PodStatsSummaryHandler(f PodStatsSummaryHandlerFunc) http.Handler {
if f == nil {
return http.HandlerFunc(NotImplemented)
@@ -97,6 +122,26 @@ func PodStatsSummaryHandler(f PodStatsSummaryHandlerFunc) http.Handler {
return r
}
// PodMetricsResourceHandler creates an http handler for serving pod metrics.
//
// If the passed in handler func is nil this will create handlers which only
// serves http.StatusNotImplemented
func PodMetricsResourceHandler(f PodMetricsResourceHandlerFunc) http.Handler {
if f == nil {
return http.HandlerFunc(NotImplemented)
}
r := mux.NewRouter()
h := HandlePodMetricsResource(f)
r.Handle(MetricsResourceRouteSuffix, ochttp.WithRouteTag(h, "PodMetricsResourceHandler")).Methods("GET")
r.Handle(MetricsResourceRouteSuffix+"/", ochttp.WithRouteTag(h, "PodMetricsResourceHandler")).Methods("GET")
r.NotFoundHandler = http.HandlerFunc(NotFound)
return r
}
// AttachPodRoutes adds the http routes for pod stuff to the passed in serve mux.
//
// Callers should take care to namespace the serve mux as they see fit, however
@@ -111,7 +156,8 @@ func AttachPodRoutes(p PodHandlerConfig, mux ServeMux, debug bool) {
// The main reason for this struct is in case of expansion we do not need to break
// the package level API.
type PodMetricsConfig struct {
GetStatsSummary PodStatsSummaryHandlerFunc
GetStatsSummary PodStatsSummaryHandlerFunc
GetMetricsResource PodMetricsResourceHandlerFunc
}
// AttachPodMetricsRoutes adds the http routes for pod/node metrics to the passed in serve mux.
@@ -120,6 +166,7 @@ type PodMetricsConfig struct {
// these routes get called by the Kubernetes API server.
func AttachPodMetricsRoutes(p PodMetricsConfig, mux ServeMux) {
mux.Handle("/", InstrumentHandler(HandlePodStatsSummary(p.GetStatsSummary)))
mux.Handle("/", InstrumentHandler(HandlePodMetricsResource(p.GetMetricsResource)))
}
func instrumentRequest(r *http.Request) *http.Request {

View File

@@ -20,11 +20,11 @@ import (
"net/http"
"github.com/pkg/errors"
stats "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
statsv1alpha1 "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
)
// PodStatsSummaryHandlerFunc defines the handler for getting pod stats summaries
type PodStatsSummaryHandlerFunc func(context.Context) (*stats.Summary, error)
type PodStatsSummaryHandlerFunc func(context.Context) (*statsv1alpha1.Summary, error)
// HandlePodStatsSummary makes an HTTP handler for implementing the kubelet summary stats endpoint
func HandlePodStatsSummary(h PodStatsSummaryHandlerFunc) http.HandlerFunc {
@@ -48,6 +48,7 @@ func HandlePodStatsSummary(h PodStatsSummaryHandlerFunc) http.HandlerFunc {
if _, err := w.Write(b); err != nil {
return errors.Wrap(err, "could not write to client")
}
_, _ = w.Write([]byte("\n"))
return nil
})
}

View File

@@ -7,7 +7,7 @@ import (
"testing"
"time"
"github.com/bombsimon/logrusr"
"github.com/bombsimon/logrusr/v3"
"github.com/sirupsen/logrus"
"github.com/virtual-kubelet/virtual-kubelet/log"
logruslogger "github.com/virtual-kubelet/virtual-kubelet/log/logrus"
@@ -18,7 +18,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/client-go/kubernetes"
klogv2 "k8s.io/klog/v2"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
@@ -65,7 +65,7 @@ func wrapE2ETest(ctx context.Context, env *envtest.Environment, f func(context.C
// The following requires that E2E tests are performed *sequentially*
log.L = logger
klogv2.SetLogger(logrusr.NewLogger(sl))
klog.SetLogger(logrusr.New(sl))
f(ctx, t, env)
}
}

View File

@@ -30,7 +30,7 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
coordclientset "k8s.io/client-go/kubernetes/typed/coordination/v1"
"k8s.io/utils/clock"
"k8s.io/utils/pointer"
"k8s.io/utils/ptr"
)
// Code heavily borrowed from: https://github.com/kubernetes/kubernetes/blob/v1.18.13/pkg/kubelet/nodelease/controller.go
@@ -78,15 +78,15 @@ func newLeaseControllerWithRenewInterval(
nodeController *NodeController) (*leaseController, error) {
if leaseDurationSeconds <= 0 {
return nil, fmt.Errorf("Lease duration seconds %d is invalid, it must be > 0", leaseDurationSeconds)
return nil, fmt.Errorf("lease duration seconds %d is invalid, it must be > 0", leaseDurationSeconds)
}
if renewInterval == 0 {
return nil, fmt.Errorf("Lease renew interval %s is invalid, it must be > 0", renewInterval.String())
return nil, fmt.Errorf("lease renew interval %s is invalid, it must be > 0", renewInterval.String())
}
if float64(leaseDurationSeconds) <= renewInterval.Seconds() {
return nil, fmt.Errorf("Lease renew interval %s is invalid, it must be less than lease duration seconds %d", renewInterval.String(), leaseDurationSeconds)
return nil, fmt.Errorf("lease renew interval %s is invalid, it must be less than lease duration seconds %d", renewInterval.String(), leaseDurationSeconds)
}
return &leaseController{
@@ -114,6 +114,7 @@ func (c *leaseController) sync(ctx context.Context) {
pingResult, err := c.nodeController.nodePingController.getResult(ctx)
if err != nil {
log.G(ctx).WithError(err).Error("Could not get ping status")
return
}
if pingResult.error != nil {
log.G(ctx).WithError(pingResult.error).Error("Ping result is not clean, not updating lease")
@@ -127,7 +128,7 @@ func (c *leaseController) sync(ctx context.Context) {
return
}
if node == nil {
err = errors.New("Servernode is null")
err = errors.New("server node is null")
log.G(ctx).WithError(err).Error("servernode is null")
span.SetStatus(err)
return
@@ -274,8 +275,8 @@ func (c *leaseController) newLease(ctx context.Context, node *corev1.Node, base
Namespace: corev1.NamespaceNodeLease,
},
Spec: coordinationv1.LeaseSpec{
HolderIdentity: pointer.StringPtr(node.Name),
LeaseDurationSeconds: pointer.Int32Ptr(c.leaseDurationSeconds),
HolderIdentity: ptr.To(node.Name),
LeaseDurationSeconds: ptr.To(c.leaseDurationSeconds),
},
}
} else {
@@ -332,7 +333,7 @@ func (e *nodeNotReadyError) Is(target error) bool {
return ok
}
func (e *nodeNotReadyError) As(target error) bool {
func (e *nodeNotReadyError) As(target interface{}) bool {
val, ok := target.(*nodeNotReadyError)
if ok {
*val = *e

View File

@@ -28,7 +28,7 @@ import (
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
watchutils "k8s.io/client-go/tools/watch"
"k8s.io/klog"
"k8s.io/klog/v2"
)
var (
@@ -38,7 +38,7 @@ var (
const (
// There might be a constant we can already leverage here
testNamespace = "default"
informerResyncPeriod = time.Duration(1 * time.Second)
informerResyncPeriod = 1 * time.Second
testNodeName = "testnode"
podSyncWorkers = 3
)
@@ -58,7 +58,7 @@ type fakeDiscardingRecorder struct {
}
func (r *fakeDiscardingRecorder) Event(object runtime.Object, eventType, reason, message string) {
r.Eventf(object, eventType, reason, message)
r.Eventf(object, eventType, reason, "%s", message)
}
func (r *fakeDiscardingRecorder) Eventf(object runtime.Object, eventType, reason, messageFmt string, args ...interface{}) {
@@ -232,7 +232,7 @@ type system struct {
}
func (s *system) start(ctx context.Context) error {
go s.pc.Run(ctx, podSyncWorkers) // nolint:errcheck
go s.pc.Run(ctx, podSyncWorkers) //nolint:errcheck
select {
case <-s.pc.Ready():
case <-s.pc.Done():
@@ -367,6 +367,14 @@ func testDanglingPodScenario(ctx context.Context, t *testing.T, s *system, m tes
}
func sendErr(ctx context.Context, ch chan error, err error) {
select {
case <-ctx.Done():
log.G(ctx).WithError(err).Warn("timeout waiting to send test error")
case ch <- err:
}
}
func testDanglingPodScenarioWithDeletionTimestamp(ctx context.Context, t *testing.T, s *system, m testingProvider) {
t.Parallel()
@@ -390,18 +398,18 @@ func testDanglingPodScenarioWithDeletionTimestamp(ctx context.Context, t *testin
_, e := s.client.CoreV1().Pods(testNamespace).Create(ctx, podCopyWithDeletionTimestamp, metav1.CreateOptions{})
assert.NilError(t, e)
// Start the pod controller
assert.NilError(t, s.start(ctx))
watchErrCh := make(chan error)
go func() {
_, watchErr := watchutils.UntilWithoutRetry(ctx, watcher,
func(ev watch.Event) (bool, error) {
return ev.Type == watch.Deleted, nil
})
watchErrCh <- watchErr
sendErr(ctx, watchErrCh, watchErr)
}()
// Start the pod controller
assert.NilError(t, s.start(ctx))
select {
case <-ctx.Done():
t.Fatalf("Context ended early: %s", ctx.Err().Error())
@@ -433,10 +441,10 @@ func testCreateStartDeleteScenario(ctx context.Context, t *testing.T, s *system,
// TODO(Sargun): Make this "smarter" about the status the pod is in.
func(ev watch.Event) (bool, error) {
pod := ev.Object.(*corev1.Pod)
return pod.Name == p.ObjectMeta.Name, nil
return pod.Name == p.Name, nil
})
watchErrCh <- watchErr
sendErr(ctx, watchErrCh, watchErr)
}()
// Create the Pod
@@ -465,7 +473,7 @@ func testCreateStartDeleteScenario(ctx context.Context, t *testing.T, s *system,
return pod.Status.Phase == corev1.PodRunning, nil
})
watchErrCh <- watchErr
sendErr(ctx, watchErrCh, watchErr)
}()
assert.NilError(t, s.start(ctx))
@@ -487,7 +495,7 @@ func testCreateStartDeleteScenario(ctx context.Context, t *testing.T, s *system,
_, watchDeleteErr := watchutils.UntilWithoutRetry(ctx, watcher2, func(ev watch.Event) (bool, error) {
return ev.Type == watch.Deleted, nil
})
waitDeleteCh <- watchDeleteErr
sendErr(ctx, waitDeleteCh, watchDeleteErr)
}()
// Setup a watch prior to pod deletion
@@ -495,7 +503,7 @@ func testCreateStartDeleteScenario(ctx context.Context, t *testing.T, s *system,
assert.NilError(t, err)
defer watcher.Stop()
go func() {
watchErrCh <- waitFunction(ctx, watcher)
sendErr(ctx, watchErrCh, waitFunction(ctx, watcher))
}()
// Delete the pod via deletiontimestamp
@@ -559,7 +567,7 @@ func testUpdatePodWhileRunningScenario(ctx context.Context, t *testing.T, s *sys
})
// This deepcopy is required to please the race detector
p = newPod.Object.(*corev1.Pod).DeepCopy()
watchErrCh <- watchErr
sendErr(ctx, watchErrCh, watchErr)
}()
// Start the pod controller
@@ -620,7 +628,7 @@ func benchmarkCreatePods(ctx context.Context, b *testing.B, s *system) {
type podModifier func(*corev1.Pod)
func randomizeUID(pod *corev1.Pod) {
pod.ObjectMeta.UID = uuid.NewUUID()
pod.UID = uuid.NewUUID()
}
func randomizeName(pod *corev1.Pod) {
@@ -630,7 +638,7 @@ func randomizeName(pod *corev1.Pod) {
func forRealAPIServer(pod *corev1.Pod) {
pod.ResourceVersion = ""
pod.ObjectMeta.UID = ""
pod.UID = ""
}
func nameBasedOnTest(t *testing.T) podModifier {

View File

@@ -6,14 +6,14 @@ import (
"os"
"testing"
klogv1 "k8s.io/klog"
"k8s.io/klog/v2"
)
var enableEnvTest = flag.Bool("envtest", false, "Enable envtest based tests")
func TestMain(m *testing.M) {
flagset := flag.NewFlagSet("klog", flag.PanicOnError)
klogv1.InitFlags(flagset)
klog.InitFlags(flagset)
flagset.VisitAll(func(f *flag.Flag) {
flag.Var(f.Value, "klog."+f.Name, f.Usage)
})

View File

@@ -243,15 +243,15 @@ func buildKeyFromNames(namespace string, name string) (string, error) {
// buildKey is a helper for building the "key" for the providers pod store.
func buildKey(pod *v1.Pod) (string, error) {
if pod.ObjectMeta.Namespace == "" {
if pod.Namespace == "" {
return "", fmt.Errorf("pod namespace not found")
}
if pod.ObjectMeta.Name == "" {
if pod.Name == "" {
return "", fmt.Errorf("pod name not found")
}
return buildKeyFromNames(pod.ObjectMeta.Namespace, pod.ObjectMeta.Name)
return buildKeyFromNames(pod.Namespace, pod.Name)
}
type mockProviderAsync struct {

View File

@@ -54,7 +54,7 @@ var (
//
// Note: Implementers can choose to manage a node themselves, in which case
// it is not needed to provide an implementation for this interface.
type NodeProvider interface { // nolint:golint
type NodeProvider interface { //nolint:revive
// Ping checks if the node is still active.
// This is intended to be lightweight as it will be called periodically as a
// heartbeat to keep the node marked as ready in Kubernetes.
@@ -84,6 +84,7 @@ func NewNodeController(p NodeProvider, node *corev1.Node, nodes v1.NodeInterface
serverNode: node,
nodes: nodes,
chReady: make(chan struct{}),
chDone: make(chan struct{}),
}
for _, o := range opts {
if err := o(n); err != nil {
@@ -104,7 +105,7 @@ func NewNodeController(p NodeProvider, node *corev1.Node, nodes v1.NodeInterface
}
// NodeControllerOpt are the functional options used for configuring a node
type NodeControllerOpt func(*NodeController) error // nolint:golint
type NodeControllerOpt func(*NodeController) error //nolint:revive
// WithNodeEnableLeaseV1 enables support for v1 leases.
// V1 Leases share all the same properties as v1beta1 leases, except they do not fallback like
@@ -145,7 +146,7 @@ func WithNodeEnableLeaseV1WithRenewInterval(client coordclientset.LeaseInterface
n,
)
if err != nil {
return fmt.Errorf("Unable to configure lease controller: %w", err)
return fmt.Errorf("unable to configure lease controller: %w", err)
}
n.leaseController = leaseController
@@ -207,7 +208,7 @@ type ErrorHandler func(context.Context, error) error
// NodeController deals with creating and managing a node object in Kubernetes.
// It can register a node with Kubernetes and periodically update its status.
// NodeController manages a single node entity.
type NodeController struct { // nolint:golint
type NodeController struct { //nolint:revive
p NodeProvider
// serverNode must be updated each time it is updated in API Server
@@ -223,7 +224,12 @@ type NodeController struct { // nolint:golint
nodeStatusUpdateErrorHandler ErrorHandler
// chReady is closed once the controller is ready to start the control loop
chReady chan struct{}
// chDone is closed once the control loop has exited
chDone chan struct{}
errMu sync.Mutex
err error
nodePingController *nodePingController
pingTimeout *time.Duration
@@ -249,7 +255,14 @@ const (
// node status update (because some things still expect the node to be updated
// periodically), otherwise it will only use node status update with the configured
// ping interval.
func (n *NodeController) Run(ctx context.Context) error {
func (n *NodeController) Run(ctx context.Context) (retErr error) {
defer func() {
n.errMu.Lock()
n.err = retErr
n.errMu.Unlock()
close(n.chDone)
}()
n.chStatusUpdate = make(chan *corev1.Node, 1)
n.p.NotifyNodeStatus(ctx, func(node *corev1.Node) {
n.chStatusUpdate <- node
@@ -273,6 +286,22 @@ func (n *NodeController) Run(ctx context.Context) error {
return n.controlLoop(ctx, providerNode)
}
// Done signals to the caller when the controller is done and the control loop is exited.
//
// Call n.Err() to find out if there was an error.
func (n *NodeController) Done() <-chan struct{} {
return n.chDone
}
// Err returns any errors that have occurred that trigger the control loop to exit.
//
// Err only returns a non-nil error after `<-n.Done()` returns.
func (n *NodeController) Err() error {
n.errMu.Lock()
defer n.errMu.Unlock()
return n.err
}
func (n *NodeController) ensureNode(ctx context.Context, providerNode *corev1.Node) (err error) {
ctx, span := trace.StartSpan(ctx, "node.ensureNode")
defer span.End()
@@ -298,28 +327,28 @@ func (n *NodeController) ensureNode(ctx context.Context, providerNode *corev1.No
n.serverNodeLock.Unlock()
// Bad things will happen if the node is deleted in k8s and recreated by someone else
// we rely on this persisting
providerNode.ObjectMeta.Name = node.Name
providerNode.ObjectMeta.Namespace = node.Namespace
providerNode.ObjectMeta.UID = node.UID
providerNode.Name = node.Name
providerNode.Namespace = node.Namespace
providerNode.UID = node.UID
return nil
}
// Ready returns a channel that gets closed when the node is fully up and
// running. Note that if there is an error on startup this channel will never
// be started.
// be closed.
func (n *NodeController) Ready() <-chan struct{} {
return n.chReady
}
func (n *NodeController) controlLoop(ctx context.Context, providerNode *corev1.Node) error {
close(n.chReady)
defer n.group.Wait()
var sleepInterval time.Duration
if n.leaseController == nil {
log.G(ctx).WithField("pingInterval", n.pingInterval).Debug("lease controller is not enabled, updating node status in Kube API server at Ping Time Interval")
log.G(ctx).
WithField("pingInterval", n.pingInterval).
Debug("lease controller is not enabled, updating node status in Kube API server at Ping Time Interval")
sleepInterval = n.pingInterval
} else {
log.G(ctx).WithField("statusInterval", n.statusInterval).Debug("lease controller in use, updating at statusInterval")
@@ -342,8 +371,8 @@ func (n *NodeController) controlLoop(ctx context.Context, providerNode *corev1.N
log.G(ctx).Debug("Received node status update")
providerNode.Status = updated.Status
providerNode.ObjectMeta.Annotations = updated.Annotations
providerNode.ObjectMeta.Labels = updated.Labels
providerNode.Annotations = updated.Annotations
providerNode.Labels = updated.Labels
if err := n.updateStatus(ctx, providerNode, false); err != nil {
log.G(ctx).WithError(err).Error("Error handling node status update")
}
@@ -355,6 +384,7 @@ func (n *NodeController) controlLoop(ctx context.Context, providerNode *corev1.N
return false
}
close(n.chReady)
for {
shouldTerminate := loop()
if shouldTerminate {
@@ -373,7 +403,7 @@ func (n *NodeController) updateStatus(ctx context.Context, providerNode *corev1.
if result, err := n.nodePingController.getResult(ctx); err != nil {
return err
} else if result.error != nil {
return fmt.Errorf("Not updating node status because node ping failed: %w", result.error)
return fmt.Errorf("not updating node status because node ping failed: %w", result.error)
}
updateNodeStatusHeartbeat(providerNode)
@@ -401,7 +431,7 @@ func (n *NodeController) updateStatus(ctx context.Context, providerNode *corev1.
}
// Returns a copy of the server node object
func (n *NodeController) getServerNode(ctx context.Context) (*corev1.Node, error) {
func (n *NodeController) getServerNode(_ context.Context) (*corev1.Node, error) {
n.serverNodeLock.Lock()
defer n.serverNodeLock.Unlock()
if n.serverNode == nil {
@@ -657,10 +687,9 @@ func (t taintsStringer) String() string {
func addNodeAttributes(ctx context.Context, span trace.Span, n *corev1.Node) context.Context {
return span.WithFields(ctx, log.Fields{
"node.UID": string(n.UID),
"node.name": n.Name,
"node.cluster": n.ClusterName,
"node.taints": taintsStringer(n.Spec.Taints),
"node.UID": string(n.UID),
"node.name": n.Name,
"node.taints": taintsStringer(n.Spec.Taints),
})
}

View File

@@ -25,7 +25,6 @@ import (
"gotest.tools/assert"
"gotest.tools/assert/cmp"
is "gotest.tools/assert/cmp"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -65,16 +64,13 @@ func testNodeRun(t *testing.T, enableLease bool) {
node, err := NewNodeController(testP, testNode, nodes, opts...)
assert.NilError(t, err)
chErr := make(chan error)
defer func() {
cancel()
assert.NilError(t, <-chErr)
<-node.Done()
assert.NilError(t, node.Err())
}()
go func() {
chErr <- node.Run(ctx)
close(chErr)
}()
go node.Run(ctx) //nolint:errcheck
nw := makeWatch(ctx, t, nodes, testNodeCopy.Name)
defer nw.Stop()
@@ -103,8 +99,8 @@ func testNodeRun(t *testing.T, enableLease bool) {
case <-time.After(time.Second):
t.Errorf("timeout waiting for event")
continue
case err := <-chErr:
t.Fatal(err) // if this returns at all it is an error regardless if err is nil
case <-node.Done():
t.Fatal(node.Err()) // if this returns at all it is an error regardless if err is nil
case <-nr:
nodeUpdates++
continue
@@ -152,8 +148,8 @@ func testNodeRun(t *testing.T, enableLease bool) {
defer eCancel()
select {
case err := <-chErr:
t.Fatal(err) // if this returns at all it is an error regardless if err is nil
case <-node.Done():
t.Fatal(node.Err()) // if this returns at all it is an error regardless if err is nil
case err := <-waitForEvent(eCtx, nr, func(e watch.Event) bool {
node := e.Object.(*corev1.Node)
if len(node.Status.Conditions) == 0 {
@@ -192,10 +188,7 @@ func TestNodeCustomUpdateStatusErrorHandler(t *testing.T) {
)
assert.NilError(t, err)
chErr := make(chan error, 1)
go func() {
chErr <- node.Run(ctx)
}()
go node.Run(ctx) //nolint:errcheck
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
@@ -204,8 +197,8 @@ func TestNodeCustomUpdateStatusErrorHandler(t *testing.T) {
select {
case <-timer.C:
t.Fatal("timeout waiting for node to be ready")
case <-chErr:
t.Fatalf("node.Run returned earlier than expected: %v", err)
case <-node.Done():
t.Fatalf("node.Run returned earlier than expected: %v", node.Err())
case <-node.Ready():
}
@@ -218,8 +211,8 @@ func TestNodeCustomUpdateStatusErrorHandler(t *testing.T) {
defer timer.Stop()
select {
case err := <-chErr:
assert.Equal(t, err, nil)
case <-node.Done():
assert.NilError(t, node.Err())
case <-timer.C:
t.Fatal("timeout waiting for node shutdown")
}
@@ -301,9 +294,11 @@ func TestPingAfterStatusUpdate(t *testing.T) {
node, err := NewNodeController(testP, testNode, nodes, opts...)
assert.NilError(t, err)
chErr := make(chan error, 1)
go func() {
chErr <- node.Run(ctx)
go node.Run(ctx) //nolint:errcheck
defer func() {
cancel()
<-node.Done()
assert.NilError(t, node.Err())
}()
timer := time.NewTimer(10 * time.Second)
@@ -313,10 +308,11 @@ func TestPingAfterStatusUpdate(t *testing.T) {
select {
case <-timer.C:
t.Fatal("timeout waiting for node to be ready")
case <-chErr:
t.Fatalf("node.Run returned earlier than expected: %v", err)
case <-node.Done():
t.Fatalf("node.Run returned earlier than expected: %v", node.Err())
case <-node.Ready():
}
timer.Stop()
notifyTimer := time.After(interval * time.Duration(10))
<-notifyTimer
@@ -360,16 +356,13 @@ func TestBeforeAnnotationsPreserved(t *testing.T) {
node, err := NewNodeController(testP, testNode, nodes, opts...)
assert.NilError(t, err)
chErr := make(chan error)
defer func() {
cancel()
assert.NilError(t, <-chErr)
<-node.Done()
assert.NilError(t, node.Err())
}()
go func() {
chErr <- node.Run(ctx)
close(chErr)
}()
go node.Run(ctx) //nolint:errcheck
nw := makeWatch(ctx, t, nodes, testNodeCopy.Name)
defer nw.Stop()
@@ -400,8 +393,8 @@ func TestBeforeAnnotationsPreserved(t *testing.T) {
newNode, err := nodes.Get(ctx, testNodeCopy.Name, emptyGetOptions)
assert.NilError(t, err)
assert.Assert(t, is.Contains(newNode.Annotations, "testAnnotation"))
assert.Assert(t, is.Contains(newNode.Annotations, "beforeAnnotation"))
assert.Assert(t, cmp.Contains(newNode.Annotations, "testAnnotation"))
assert.Assert(t, cmp.Contains(newNode.Annotations, "beforeAnnotation"))
}
// Are conditions set by systems outside of VK preserved?
@@ -427,16 +420,13 @@ func TestManualConditionsPreserved(t *testing.T) {
node, err := NewNodeController(testP, testNode, nodes, opts...)
assert.NilError(t, err)
chErr := make(chan error)
defer func() {
cancel()
assert.NilError(t, <-chErr)
<-node.Done()
assert.NilError(t, node.Err())
}()
go func() {
chErr <- node.Run(ctx)
close(chErr)
}()
go node.Run(ctx) //nolint:errcheck
nw := makeWatch(ctx, t, nodes, testNodeCopy.Name)
defer nw.Stop()
@@ -453,7 +443,7 @@ func TestManualConditionsPreserved(t *testing.T) {
newNode, err := nodes.Get(ctx, testNodeCopy.Name, emptyGetOptions)
assert.NilError(t, err)
assert.Assert(t, is.Len(newNode.Status.Conditions, 0))
assert.Assert(t, cmp.Len(newNode.Status.Conditions, 0))
baseCondition := corev1.NodeCondition{
Type: "BaseCondition",
@@ -488,8 +478,8 @@ func TestManualConditionsPreserved(t *testing.T) {
newNode, err = nodes.Get(ctx, testNodeCopy.Name, emptyGetOptions)
assert.NilError(t, err)
assert.Assert(t, is.Len(newNode.Status.Conditions, 1))
assert.Assert(t, is.Contains(newNode.Annotations, "testAnnotation"))
assert.Assert(t, cmp.Len(newNode.Status.Conditions, 1))
assert.Assert(t, cmp.Contains(newNode.Annotations, "testAnnotation"))
// Add a new event manually
manuallyAddedCondition := corev1.NodeCondition{
@@ -516,8 +506,8 @@ func TestManualConditionsPreserved(t *testing.T) {
return true
}
}
assert.Assert(t, is.Contains(receivedNode.Annotations, "testAnnotation"))
assert.Assert(t, is.Contains(newNode.Annotations, "manuallyAddedAnnotation"))
assert.Assert(t, cmp.Contains(receivedNode.Annotations, "testAnnotation"))
assert.Assert(t, cmp.Contains(newNode.Annotations, "manuallyAddedAnnotation"))
return false
}))
@@ -562,11 +552,11 @@ func TestManualConditionsPreserved(t *testing.T) {
for idx := range newNode.Status.Conditions {
seenConditionTypes[idx] = newNode.Status.Conditions[idx].Type
}
assert.Assert(t, is.Contains(seenConditionTypes, baseCondition.Type))
assert.Assert(t, is.Contains(seenConditionTypes, newCondition.Type))
assert.Assert(t, is.Contains(seenConditionTypes, manuallyAddedCondition.Type))
assert.Assert(t, is.Equal(newNode.Annotations["testAnnotation"], ""))
assert.Assert(t, is.Contains(newNode.Annotations, "manuallyAddedAnnotation"))
assert.Assert(t, cmp.Contains(seenConditionTypes, baseCondition.Type))
assert.Assert(t, cmp.Contains(seenConditionTypes, newCondition.Type))
assert.Assert(t, cmp.Contains(seenConditionTypes, manuallyAddedCondition.Type))
assert.Assert(t, cmp.Equal(newNode.Annotations["testAnnotation"], ""))
assert.Assert(t, cmp.Contains(newNode.Annotations, "manuallyAddedAnnotation"))
t.Log(newNode.Status.Conditions)
}
@@ -616,15 +606,15 @@ func TestNodePingSingleInflight(t *testing.T) {
assert.Assert(t, timeTakenToCompleteFirstPing < pingTimeout*5, "Time taken to complete first ping: %v", timeTakenToCompleteFirstPing)
assert.Assert(t, cmp.Error(firstPing.error, context.DeadlineExceeded.Error()))
assert.Assert(t, is.Equal(1, calls.read()))
assert.Assert(t, is.Equal(0, finished.read()))
assert.Assert(t, cmp.Equal(1, calls.read()))
assert.Assert(t, cmp.Equal(0, finished.read()))
// Wait until the first sleep finishes (the test context is done)
assert.NilError(t, finished.until(testCtx, func(i int) bool { return i > 0 }))
// Assert we didn't stack up goroutines, and that the one goroutine in flight finishd
assert.Assert(t, is.Equal(1, calls.read()))
assert.Assert(t, is.Equal(1, finished.read()))
assert.Assert(t, cmp.Equal(1, calls.read()))
assert.Assert(t, cmp.Equal(1, finished.read()))
}

234
node/nodeutil/auth.go Normal file
View File

@@ -0,0 +1,234 @@
package nodeutil
import (
"context"
"net/http"
"strings"
"time"
"github.com/virtual-kubelet/virtual-kubelet/errdefs"
"github.com/virtual-kubelet/virtual-kubelet/log"
"github.com/virtual-kubelet/virtual-kubelet/trace"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
"k8s.io/apiserver/pkg/authentication/request/anonymous"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
"k8s.io/apiserver/pkg/server/options"
"k8s.io/client-go/kubernetes"
)
// Auth is the interface used to implement authn/authz for http requests
type Auth interface {
authenticator.Request
authorizer.RequestAttributesGetter
authorizer.Authorizer
}
type authWrapper struct {
authenticator.Request
authorizer.RequestAttributesGetter
authorizer.Authorizer
}
// InstrumentAuth wraps the provided Auth in a new instrumented Auth
//
// Note: You would only need this if you rolled your own auth.
// The Auth implementations defined in this package are already instrumented.
func InstrumentAuth(auth Auth) Auth {
if _, ok := auth.(*authWrapper); ok {
// This is already instrumented
return auth
}
return &authWrapper{
Request: auth,
RequestAttributesGetter: auth,
Authorizer: auth,
}
}
// NoAuth creates an Auth which allows anonymous access to all resouorces
func NoAuth() Auth {
return &authWrapper{
Request: anonymous.NewAuthenticator(nil),
RequestAttributesGetter: &NodeRequestAttr{},
Authorizer: authorizerfactory.NewAlwaysAllowAuthorizer(),
}
}
// WithAuth makes a new http handler which wraps the provided handler with authn/authz.
func WithAuth(auth Auth, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handleAuth(auth, w, r, h)
})
}
func handleAuth(auth Auth, w http.ResponseWriter, r *http.Request, next http.Handler) {
ctx := r.Context()
ctx, span := trace.StartSpan(ctx, "vk.handleAuth")
defer span.End()
r = r.WithContext(ctx)
logger := log.G(r.Context())
info, ok, err := auth.AuthenticateRequest(r)
if err != nil {
logger.WithError(err).Error("Error authenticating request")
http.Error(w, "Unauthorized", http.StatusUnauthorized)
span.SetStatus(err)
return
}
if !ok {
logger.Error("Request not authenticated")
log.G(r.Context()).Infof("Unauthorized: RequestURI: %s", r.RequestURI)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
span.SetStatus(errdefs.ErrUnauthorized)
return
}
logger = logger.WithFields(log.Fields{
"user-name": info.User.GetName(),
"user-id": info.User.GetUID(),
})
ctx = log.WithLogger(ctx, logger)
r = r.WithContext(ctx)
attrs := auth.GetRequestAttributes(info.User, r)
decision, _, err := auth.Authorize(ctx, attrs)
if err != nil {
log.G(r.Context()).WithError(err).Error("Authorization error")
http.Error(w, err.Error(), http.StatusInternalServerError)
span.SetStatus(err)
return
}
if decision != authorizer.DecisionAllow {
http.Error(w, "Forbidden", http.StatusForbidden)
span.SetStatus(errdefs.ErrForbidden)
return
}
next.ServeHTTP(w, r)
}
// WebhookAuthOption is used as a functional argument to configure webhook auth.
type WebhookAuthOption func(*WebhookAuthConfig) error
// WebhookAuthConfig stores the configurations for authn/authz and is used by WebhookAuthOption to expose to callers.
type WebhookAuthConfig struct {
AuthnConfig authenticatorfactory.DelegatingAuthenticatorConfig
AuthzConfig authorizerfactory.DelegatingAuthorizerConfig
}
// WebhookAuth creates an Auth suitable to use with kubelet webhook auth.
// You must provide a CA provider to the authentication config, otherwise mTLS is disabled.
func WebhookAuth(client kubernetes.Interface, nodeName string, opts ...WebhookAuthOption) (Auth, error) {
cfg := WebhookAuthConfig{
AuthnConfig: authenticatorfactory.DelegatingAuthenticatorConfig{
CacheTTL: 2 * time.Minute, // default taken from k8s.io/kubernetes/pkg/kubelet/apis/config/v1beta1
WebhookRetryBackoff: options.DefaultAuthWebhookRetryBackoff(),
},
AuthzConfig: authorizerfactory.DelegatingAuthorizerConfig{
AllowCacheTTL: 5 * time.Minute, // default taken from k8s.io/kubernetes/pkg/kubelet/apis/config/v1beta1
DenyCacheTTL: 30 * time.Second, // default taken from k8s.io/kubernetes/pkg/kubelet/apis/config/v1beta1
WebhookRetryBackoff: options.DefaultAuthWebhookRetryBackoff(),
},
}
for _, o := range opts {
if err := o(&cfg); err != nil {
return nil, err
}
}
cfg.AuthnConfig.TokenAccessReviewClient = client.AuthenticationV1()
cfg.AuthzConfig.SubjectAccessReviewClient = client.AuthorizationV1()
authn, _, err := cfg.AuthnConfig.New()
if err != nil {
return nil, err
}
authz, err := cfg.AuthzConfig.New()
if err != nil {
return nil, err
}
return &authWrapper{
Request: authn,
RequestAttributesGetter: NodeRequestAttr{nodeName},
Authorizer: authz,
}, nil
}
func (w *authWrapper) AuthenticateRequest(r *http.Request) (*authenticator.Response, bool, error) {
ctx, span := trace.StartSpan(r.Context(), "AuthenticateRequest")
defer span.End()
return w.Request.AuthenticateRequest(r.WithContext(ctx))
}
func (w *authWrapper) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
ctx, span := trace.StartSpan(ctx, "Authorize")
defer span.End()
return w.Authorizer.Authorize(ctx, a)
}
// NodeRequestAttr is a authorizor.RequeestAttributesGetter which can be used in the Auth interface.
type NodeRequestAttr struct {
NodeName string
}
// GetRequestAttributes satisfies the authorizer.RequestAttributesGetter interface for use with an `Auth`.
func (a NodeRequestAttr) GetRequestAttributes(u user.Info, r *http.Request) authorizer.Attributes {
return authorizer.AttributesRecord{
User: u,
Verb: getAPIVerb(r),
Namespace: "",
APIGroup: "",
APIVersion: "v1",
Resource: "nodes",
Name: a.NodeName,
ResourceRequest: true,
Path: r.URL.Path,
Subresource: getSubresource(r),
}
}
func getAPIVerb(r *http.Request) string {
switch r.Method {
case http.MethodPost:
return "create"
case http.MethodGet:
return "get"
case http.MethodPut:
return "update"
case http.MethodPatch:
return "patch"
case http.MethodDelete:
return "delete"
}
return ""
}
func isSubpath(subpath, path string) bool {
// Taken from k8s.io/kubernetes/pkg/kubelet/server/auth.go
return subpath == path || (strings.HasPrefix(subpath, path) && subpath[len(path)] == '/')
}
func getSubresource(r *http.Request) string {
if isSubpath(r.URL.Path, "/stats") {
return "stats"
}
if isSubpath(r.URL.Path, "/metrics") {
return "metrics"
}
if isSubpath(r.URL.Path, "/logs") {
// yes, "log", not "logs"
// per kubelet code: "log" to match other log subresources (pods/log, etc)
return "log"
}
return "proxy"
}

View File

@@ -24,7 +24,7 @@ func ClientsetFromEnv(kubeConfigPath string) (*kubernetes.Clientset, error) {
)
if kubeConfigPath != "" {
config, err = clientsetFromEnvKubeConfigPath(kubeConfigPath)
config, err = RestConfigFromEnv(kubeConfigPath)
} else {
config, err = rest.InClusterConfig()
}
@@ -36,7 +36,8 @@ func ClientsetFromEnv(kubeConfigPath string) (*kubernetes.Clientset, error) {
return kubernetes.NewForConfig(config)
}
func clientsetFromEnvKubeConfigPath(kubeConfigPath string) (*rest.Config, error) {
// RestConfigFromEnv is like ClientsetFromEnv except it returns a rest config instead of a full client.
func RestConfigFromEnv(kubeConfigPath string) (*rest.Config, error) {
_, err := os.Stat(kubeConfigPath)
if os.IsNotExist(err) {
return rest.InClusterConfig()

445
node/nodeutil/controller.go Normal file
View File

@@ -0,0 +1,445 @@
package nodeutil
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"os"
"path"
"runtime"
"time"
"github.com/pkg/errors"
"github.com/virtual-kubelet/virtual-kubelet/log"
"github.com/virtual-kubelet/virtual-kubelet/node"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/record"
)
// Node helps manage the startup/shutdown procedure for other controllers.
// It is intended as a convenience to reduce boiler plate code for starting up controllers.
//
// Must be created with constructor `NewNode`.
type Node struct {
nc *node.NodeController
pc *node.PodController
readyCb func(context.Context) error
ready chan struct{}
done chan struct{}
err error
podInformerFactory informers.SharedInformerFactory
scmInformerFactory informers.SharedInformerFactory
client kubernetes.Interface
listenAddr string
h http.Handler
tlsConfig *tls.Config
workers int
eb record.EventBroadcaster
caController caController
}
// NodeController returns the configured node controller.
func (n *Node) NodeController() *node.NodeController {
return n.nc
}
// PodController returns the configured pod controller.
func (n *Node) PodController() *node.PodController {
return n.pc
}
func (n *Node) runHTTP(ctx context.Context) (func(), error) {
if n.tlsConfig == nil {
log.G(ctx).Warn("TLS config not provided, not starting up http service")
return func() {}, nil
}
if n.h == nil {
log.G(ctx).Debug("No http handler, not starting up http service")
return func() {}, nil
}
l, err := tls.Listen("tcp", n.listenAddr, n.tlsConfig)
if err != nil {
return nil, errors.Wrap(err, "error starting http listener")
}
log.G(ctx).Debug("Started TLS listener")
srv := &http.Server{Handler: n.h, TLSConfig: n.tlsConfig, ReadHeaderTimeout: 30 * time.Second}
go srv.Serve(l) //nolint:errcheck
log.G(ctx).Debug("HTTP server running")
return func() {
/* #nosec */
srv.Close()
/* #nosec */
l.Close()
}, nil
}
// Run starts all the underlying controllers
func (n *Node) Run(ctx context.Context) (retErr error) {
ctx, cancel := context.WithCancel(ctx)
defer func() {
cancel()
n.err = retErr
close(n.done)
}()
if n.eb != nil {
n.eb.StartLogging(log.G(ctx).Infof)
n.eb.StartRecordingToSink(&corev1client.EventSinkImpl{Interface: n.client.CoreV1().Events(v1.NamespaceAll)})
defer n.eb.Shutdown()
log.G(ctx).Debug("Started event broadcaster")
}
if n.caController != nil {
go n.caController.Run(ctx, 1)
}
cancelHTTP, err := n.runHTTP(ctx)
if err != nil {
return err
}
defer cancelHTTP()
go n.podInformerFactory.Start(ctx.Done())
go n.scmInformerFactory.Start(ctx.Done())
go n.pc.Run(ctx, n.workers) //nolint:errcheck
defer func() {
cancel()
<-n.pc.Done()
}()
select {
case <-ctx.Done():
return n.err
case <-n.pc.Ready():
case <-n.pc.Done():
return n.pc.Err()
}
log.G(ctx).Debug("pod controller ready")
go n.nc.Run(ctx) //nolint:errcheck
defer func() {
cancel()
<-n.nc.Done()
}()
select {
case <-ctx.Done():
n.err = ctx.Err()
return n.err
case <-n.nc.Ready():
case <-n.nc.Done():
return n.nc.Err()
}
log.G(ctx).Debug("node controller ready")
if n.readyCb != nil {
if err := n.readyCb(ctx); err != nil {
return err
}
}
close(n.ready)
select {
case <-n.nc.Done():
cancel()
return n.nc.Err()
case <-n.pc.Done():
cancel()
return n.pc.Err()
}
}
// WaitReady waits for the specified timeout for the controller to be ready.
//
// The timeout is for convenience so the caller doesn't have to juggle an extra context.
func (n *Node) WaitReady(ctx context.Context, timeout time.Duration) error {
if timeout > 0 {
var cancel func()
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
select {
case <-n.ready:
return nil
case <-n.done:
return fmt.Errorf("controller exited before ready: %w", n.err)
case <-ctx.Done():
return ctx.Err()
}
}
// Ready returns a channel that will be closed after the controller is ready.
func (n *Node) Ready() <-chan struct{} {
return n.ready
}
// Done returns a channel that will be closed when the controller has exited.
func (n *Node) Done() <-chan struct{} {
return n.done
}
// Err returns any error that occurred with the controller.
//
// This always return nil before `<-Done()`.
func (n *Node) Err() error {
select {
case <-n.Done():
return n.err
default:
return nil
}
}
// NodeOpt is used as functional options when configuring a new node in NewNodeFromClient
type NodeOpt func(c *NodeConfig) error
type caController interface{ Run(context.Context, int) }
// NodeConfig is used to hold configuration items for a Node.
// It gets used in conjection with NodeOpt in NewNodeFromClient
type NodeConfig struct {
// Set the client to use, otherwise a client will be created from ClientsetFromEnv
Client kubernetes.Interface
// Set the node spec to register with Kubernetes
NodeSpec v1.Node
// Set the path to read a kubeconfig from for creating a client.
// This is ignored when a client is provided to NewNodeFromClient
KubeconfigPath string
// Set the period for a full resync for generated client-go informers
InformerResyncPeriod time.Duration
// Set the address to listen on for the http API
HTTPListenAddr string
// Set a custom API handler to use.
// You can use this to setup, for example, authentication middleware.
// If one is not provided a default one will be created.
//
// Note: If you provide your own handler, you'll need to handle all auth, routes, etc.
Handler http.Handler
// Set the timeout for idle http streams
StreamIdleTimeout time.Duration
// Set the timeout for creating http streams
StreamCreationTimeout time.Duration
// Enable http debugging routes
DebugHTTP bool
// Set the tls config to use for the http server
TLSConfig *tls.Config
// Specify the event recorder to use
// If this is not provided, a default one will be used.
EventRecorder record.EventRecorder
// Set the number of workers to reconcile pods
// The default value is derived from the number of cores available.
NumWorkers int
// Set the error handler for node status update failures
NodeStatusUpdateErrorHandler node.ErrorHandler
// SkipDownwardAPIResolution can be used to skip any attempts at resolving downward API references
// in pods before calling CreatePod on the provider.
// Providers need this if they need to do their own custom resolving
SkipDownwardAPIResolution bool
routeAttacher func(Provider, NodeConfig, corev1listers.PodLister)
caController caController
}
// NewNode creates a new node using the provided client and name.
// This is intended for high-level/low boiler-plate usage.
// Use the constructors in the `node` package for lower level configuration.
//
// Some basic values are set for node status, you'll almost certainly want to modify it.
//
// If client is nil, this will construct a client using ClientsetFromEnv
// It is up to the caller to configure auth on the HTTP handler.
func NewNode(name string, newProvider NewProviderFunc, opts ...NodeOpt) (*Node, error) {
cfg := NodeConfig{
NumWorkers: runtime.NumCPU(),
InformerResyncPeriod: time.Minute,
KubeconfigPath: os.Getenv("KUBECONFIG"),
HTTPListenAddr: ":10250",
NodeSpec: v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: map[string]string{
"type": "virtual-kubelet",
"kubernetes.io/role": "agent",
"kubernetes.io/hostname": name,
},
},
Status: v1.NodeStatus{
Phase: v1.NodePending,
Conditions: []v1.NodeCondition{
{Type: v1.NodeReady},
{Type: v1.NodeDiskPressure},
{Type: v1.NodeMemoryPressure},
{Type: v1.NodePIDPressure},
{Type: v1.NodeNetworkUnavailable},
},
},
},
}
cfg.Client = defaultClientFromEnv(cfg.KubeconfigPath)
for _, o := range opts {
if err := o(&cfg); err != nil {
return nil, err
}
}
if _, _, err := net.SplitHostPort(cfg.HTTPListenAddr); err != nil {
return nil, errors.Wrap(err, "error parsing http listen address")
}
podInformerFactory := informers.NewSharedInformerFactoryWithOptions(
cfg.Client,
cfg.InformerResyncPeriod,
PodInformerFilter(name),
)
scmInformerFactory := informers.NewSharedInformerFactoryWithOptions(
cfg.Client,
cfg.InformerResyncPeriod,
)
podInformer := podInformerFactory.Core().V1().Pods()
secretInformer := scmInformerFactory.Core().V1().Secrets()
configMapInformer := scmInformerFactory.Core().V1().ConfigMaps()
serviceInformer := scmInformerFactory.Core().V1().Services()
p, np, err := newProvider(ProviderConfig{
Pods: podInformer.Lister(),
ConfigMaps: configMapInformer.Lister(),
Secrets: secretInformer.Lister(),
Services: serviceInformer.Lister(),
Node: &cfg.NodeSpec,
})
if err != nil {
return nil, errors.Wrap(err, "error creating provider")
}
if cfg.routeAttacher != nil {
cfg.routeAttacher(p, cfg, podInformer.Lister())
}
var readyCb func(context.Context) error
if np == nil {
nnp := node.NewNaiveNodeProvider()
np = nnp
readyCb = func(ctx context.Context) error {
setNodeReady(&cfg.NodeSpec)
err := nnp.UpdateStatus(ctx, &cfg.NodeSpec)
return errors.Wrap(err, "error marking node as ready")
}
}
nodeControllerOpts := []node.NodeControllerOpt{
node.WithNodeEnableLeaseV1(NodeLeaseV1Client(cfg.Client), node.DefaultLeaseDuration),
}
if cfg.NodeStatusUpdateErrorHandler != nil {
nodeControllerOpts = append(nodeControllerOpts, node.WithNodeStatusUpdateErrorHandler(cfg.NodeStatusUpdateErrorHandler))
}
nc, err := node.NewNodeController(
np,
&cfg.NodeSpec,
cfg.Client.CoreV1().Nodes(),
nodeControllerOpts...,
)
if err != nil {
return nil, errors.Wrap(err, "error creating node controller")
}
var eb record.EventBroadcaster
if cfg.EventRecorder == nil {
eb = record.NewBroadcaster()
cfg.EventRecorder = eb.NewRecorder(scheme.Scheme, v1.EventSource{Component: path.Join(name, "pod-controller")})
}
pc, err := node.NewPodController(node.PodControllerConfig{
PodClient: cfg.Client.CoreV1(),
EventRecorder: cfg.EventRecorder,
Provider: p,
PodInformer: podInformer,
SecretInformer: secretInformer,
ConfigMapInformer: configMapInformer,
ServiceInformer: serviceInformer,
SkipDownwardAPIResolution: cfg.SkipDownwardAPIResolution,
})
if err != nil {
return nil, errors.Wrap(err, "error creating pod controller")
}
return &Node{
nc: nc,
pc: pc,
readyCb: readyCb,
ready: make(chan struct{}),
done: make(chan struct{}),
eb: eb,
podInformerFactory: podInformerFactory,
scmInformerFactory: scmInformerFactory,
client: cfg.Client,
tlsConfig: cfg.TLSConfig,
h: cfg.Handler,
listenAddr: cfg.HTTPListenAddr,
workers: cfg.NumWorkers,
caController: cfg.caController,
}, nil
}
func setNodeReady(n *v1.Node) {
n.Status.Phase = v1.NodeRunning
for i, c := range n.Status.Conditions {
if c.Type != "Ready" {
continue
}
c.Message = "Kubelet is ready"
c.Reason = "KubeletReady"
c.Status = v1.ConditionTrue
c.LastHeartbeatTime = metav1.Now()
c.LastTransitionTime = metav1.Now()
n.Status.Conditions[i] = c
return
}
}
func defaultClientFromEnv(kubeconfigPath string) kubernetes.Interface {
client, err := ClientsetFromEnv(kubeconfigPath)
if err != nil {
log.G(context.TODO()).WithError(err).
Warn("Failed to create clientset from env. Ignore this error If you use your own client")
}
return client
}

150
node/nodeutil/opts.go Normal file
View File

@@ -0,0 +1,150 @@
package nodeutil
import (
"fmt"
"net/http"
"github.com/virtual-kubelet/virtual-kubelet/errdefs"
"github.com/virtual-kubelet/virtual-kubelet/node/api"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// WithNodeConfig returns a NodeOpt which replaces the NodeConfig with the passed in value.
func WithNodeConfig(c NodeConfig) NodeOpt {
return func(orig *NodeConfig) error {
*orig = c
return nil
}
}
// WithClient return a NodeOpt that sets the client that will be used to create/manage the node.
func WithClient(c kubernetes.Interface) NodeOpt {
return func(cfg *NodeConfig) error {
cfg.Client = c
return nil
}
}
// BootstrapConfig is the configuration for bootstrapping a node.
type BootstrapConfig struct {
// ClientCAConfigMapSpec is the config map information for getting the CA for authenticating clients
// If not set, the CA in the rest config will be used.
ClientCAConfigMapSpec *ConfigMapCASpec
// A set of options to pass when creating the webhook auth.
WebhookAuthOpts []WebhookAuthOption
// RestConfig is the rest config to use for the client
// If it is not provided a default one from the environment will be created.
RestConfig *rest.Config
}
// ConfigMapCASpec is the spec for a config map that contains a CA.
type ConfigMapCASpec struct {
Namespace string
Name string
Key string
}
// BootstrapOpt is a functional option used to configure node bootstrapping
type BootstrapOpt func(*BootstrapConfig) error
// WithBootstrapFromRestConfig takes a reset config (or a default one from the environment) and returns a NodeOpt that will:
// 1. Create a client from the rest config (if no client is set)
// 2. Create webhook authn/authz from the rest config
// 3. Configure the TLS config for the HTTP server from the certs in the rest config.
func WithBootstrapFromRestConfig(opts ...BootstrapOpt) NodeOpt {
return func(cfg *NodeConfig) error {
var bOpts BootstrapConfig
if rCfg, err := RestConfigFromEnv(cfg.KubeconfigPath); err == nil {
bOpts.RestConfig = rCfg
}
for _, o := range opts {
if err := o(&bOpts); err != nil {
return err
}
}
if bOpts.RestConfig == nil {
return fmt.Errorf("no rest config provided")
}
if cfg.Client == nil {
client, err := kubernetes.NewForConfig(bOpts.RestConfig)
if err != nil {
return err
}
cfg.Client = client
}
if err := WithTLSConfig(tlsFromRestConfig(bOpts.RestConfig))(cfg); err != nil {
return err
}
if err := configureWebhookCA(cfg, &bOpts); err != nil {
return fmt.Errorf("error configure webhook auth: %w", err)
}
var mux *http.ServeMux
if cfg.Handler == nil {
mux = http.NewServeMux()
cfg.Handler = mux
}
if cfg.routeAttacher == nil && mux != nil {
if err := AttachProviderRoutes(mux)(cfg); err != nil {
return err
}
}
auth, err := WebhookAuth(cfg.Client, cfg.NodeSpec.Name, bOpts.WebhookAuthOpts...)
if err != nil {
return err
}
cfg.Handler = api.InstrumentHandler(WithAuth(auth, cfg.Handler))
return nil
}
}
func configureWebhookCA(cfg *NodeConfig, bCfg *BootstrapConfig) error {
if bCfg.ClientCAConfigMapSpec != nil {
bCfg.WebhookAuthOpts = append(bCfg.WebhookAuthOpts, func(auth *WebhookAuthConfig) error {
cmCA, err := dynamiccertificates.NewDynamicCAFromConfigMapController("client-ca", bCfg.ClientCAConfigMapSpec.Namespace, bCfg.ClientCAConfigMapSpec.Name, bCfg.ClientCAConfigMapSpec.Key, cfg.Client)
if err != nil {
return fmt.Errorf("error loading dynamic CA from config map: %w", err)
}
auth.AuthnConfig.ClientCertificateCAContentProvider = cmCA
cfg.caController = cmCA
return nil
})
return nil
}
if bCfg.RestConfig.CAFile != "" {
bCfg.WebhookAuthOpts = append(bCfg.WebhookAuthOpts, func(auth *WebhookAuthConfig) error {
caFile, err := dynamiccertificates.NewDynamicCAContentFromFile("ca-file", bCfg.RestConfig.CAFile)
if err != nil {
return fmt.Errorf("error loading dynamic CA file from rest config: %w", err)
}
auth.AuthnConfig.ClientCertificateCAContentProvider = caFile
cfg.caController = caFile
return nil
})
return nil
}
if bCfg.RestConfig.CAData != nil {
bCfg.WebhookAuthOpts = append(bCfg.WebhookAuthOpts, func(auth *WebhookAuthConfig) error {
caData, err := dynamiccertificates.NewStaticCAContent("ca-data", bCfg.RestConfig.CAData)
if err != nil {
return fmt.Errorf("error loading static ca from rest config: %w", err)
}
auth.AuthnConfig.ClientCertificateCAContentProvider = caData
return nil
})
return nil
}
return errdefs.InvalidInput("no client CA found")
}

84
node/nodeutil/provider.go Normal file
View File

@@ -0,0 +1,84 @@
package nodeutil
import (
"context"
"io"
dto "github.com/prometheus/client_model/go"
"github.com/virtual-kubelet/virtual-kubelet/node"
"github.com/virtual-kubelet/virtual-kubelet/node/api"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
corev1listers "k8s.io/client-go/listers/core/v1"
statsv1alpha1 "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
)
// Provider contains the methods required to implement a virtual-kubelet provider.
//
// Errors produced by these methods should implement an interface from
// github.com/virtual-kubelet/virtual-kubelet/errdefs package in order for the
// core logic to be able to understand the type of failure
type Provider interface {
node.PodLifecycleHandler
// GetContainerLogs retrieves the logs of a container by name from the provider.
GetContainerLogs(ctx context.Context, namespace, podName, containerName string, opts api.ContainerLogOpts) (io.ReadCloser, error)
// RunInContainer executes a command in a container in the pod, copying data
// between in/out/err and the container's stdin/stdout/stderr.
RunInContainer(ctx context.Context, namespace, podName, containerName string, cmd []string, attach api.AttachIO) error
// AttachToContainer attaches to the executing process of a container in the pod, copying data
// between in/out/err and the container's stdin/stdout/stderr.
AttachToContainer(ctx context.Context, namespace, podName, containerName string, attach api.AttachIO) error
// GetStatsSummary gets the stats for the node, including running pods
GetStatsSummary(context.Context) (*statsv1alpha1.Summary, error)
// GetMetricsResource gets the metrics for the node, including running pods
GetMetricsResource(context.Context) ([]*dto.MetricFamily, error)
// PortForward forwards a local port to a port on the pod
PortForward(ctx context.Context, namespace, pod string, port int32, stream io.ReadWriteCloser) error
}
// ProviderConfig holds objects created by NewNodeFromClient that a provider may need to bootstrap itself.
type ProviderConfig struct {
Pods corev1listers.PodLister
ConfigMaps corev1listers.ConfigMapLister
Secrets corev1listers.SecretLister
Services corev1listers.ServiceLister
// Hack to allow the provider to set things on the node
// Since the provider is bootstrapped after the node object is configured
// Primarily this is due to carry-over from the pre-1.0 interfaces that expect the provider instead of the direct *caller* to configure the node.
Node *v1.Node
}
// NewProviderFunc is used from NewNodeFromClient to bootstrap a provider using the client/listers/etc created there.
// If a nil node provider is returned a default one will be used.
type NewProviderFunc func(ProviderConfig) (Provider, node.NodeProvider, error)
// AttachProviderRoutes returns a NodeOpt which uses api.PodHandler to attach the routes to the provider functions.
//
// Note this only attaches routes, you'll need to ensure to set the handler in the node config.
func AttachProviderRoutes(mux api.ServeMux) NodeOpt {
return func(cfg *NodeConfig) error {
cfg.routeAttacher = func(p Provider, cfg NodeConfig, pods corev1listers.PodLister) {
mux.Handle("/", api.PodHandler(api.PodHandlerConfig{
RunInContainer: p.RunInContainer,
AttachToContainer: p.AttachToContainer,
GetContainerLogs: p.GetContainerLogs,
GetPods: p.GetPods,
GetPodsFromKubernetes: func(context.Context) ([]*v1.Pod, error) {
return pods.List(labels.Everything())
},
GetStatsSummary: p.GetStatsSummary,
GetMetricsResource: p.GetMetricsResource,
StreamIdleTimeout: cfg.StreamIdleTimeout,
StreamCreationTimeout: cfg.StreamCreationTimeout,
PortForward: p.PortForward,
}, true))
}
return nil
}
}

131
node/nodeutil/tls.go Normal file
View File

@@ -0,0 +1,131 @@
package nodeutil
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"k8s.io/client-go/rest"
)
// WithTLSConfig returns a NodeOpt which creates a base TLSConfig with the default cipher suites and tls min verions.
// The tls config can be modified through functional options.
func WithTLSConfig(opts ...func(*tls.Config) error) NodeOpt {
return func(cfg *NodeConfig) error {
if cfg.TLSConfig == nil {
cfg.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
CipherSuites: DefaultServerCiphers(),
ClientAuth: tls.RequestClientCert,
}
}
for _, o := range opts {
if err := o(cfg.TLSConfig); err != nil {
return err
}
}
return nil
}
}
// WithCAFromPath makes a TLS config option to set up client auth using the path to a PEM encoded CA cert.
func WithCAFromPath(p string) func(*tls.Config) error {
return func(cfg *tls.Config) error {
pem, err := os.ReadFile(p)
if err != nil {
return fmt.Errorf("error reading ca cert pem: %w", err)
}
cfg.ClientAuth = tls.RequireAndVerifyClientCert
return WithCACert(pem)(cfg)
}
}
// WithKeyPairFromPath make sa TLS config option which loads the key pair paths from disk and appends them to the tls config.
func WithKeyPairFromPath(cert, key string) func(*tls.Config) error {
return func(cfg *tls.Config) error {
cert, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return fmt.Errorf("error loading x509 key pair: %w", err)
}
cfg.Certificates = append(cfg.Certificates, cert)
return nil
}
}
// WithCACert makes a TLS config opotion which appends the provided PEM encoded bytes the tls config's cert pool.
// If a cert pool is not defined on the tls config an empty one will be created.
func WithCACert(pem []byte) func(*tls.Config) error {
return func(cfg *tls.Config) error {
if cfg.RootCAs == nil {
cfg.ClientCAs = x509.NewCertPool()
}
if !cfg.ClientCAs.AppendCertsFromPEM(pem) {
return fmt.Errorf("could not parse ca cert pem")
}
return nil
}
}
// DefaultServerCiphers is the list of accepted TLS ciphers, with known weak ciphers elided
// Note this list should be a moving target.
func DefaultServerCiphers() []uint16 {
return []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
}
}
func tlsFromRestConfig(r *rest.Config) func(*tls.Config) error {
return func(cfg *tls.Config) error {
var err error
cfg.ClientAuth = tls.RequestClientCert
certData := r.CertData
if certData == nil && r.CertFile != "" {
certData, err = os.ReadFile(r.CertFile)
if err != nil {
return fmt.Errorf("error reading cert file from clientset: %w", err)
}
}
keyData := r.KeyData
if keyData == nil && r.KeyFile != "" {
keyData, err = os.ReadFile(r.KeyFile)
if err != nil {
return fmt.Errorf("error reading key file from clientset: %w", err)
}
}
if keyData != nil && certData != nil {
pem, err := tls.X509KeyPair(certData, keyData)
if err != nil {
return fmt.Errorf("error creating key pair from clientset: %w", err)
}
cfg.Certificates = append(cfg.Certificates, pem)
cfg.ClientAuth = tls.RequestClientCert
}
caData := r.CAData
if certData == nil && r.CAFile != "" {
caData, err = os.ReadFile(r.CAFile)
if err != nil {
return fmt.Errorf("error reading ca file from clientset: %w", err)
}
}
if caData != nil {
return WithCACert(caData)(cfg)
}
return nil
}
}

View File

@@ -20,10 +20,11 @@ import (
"strings"
"time"
"github.com/virtual-kubelet/virtual-kubelet/internal/queue"
"github.com/google/go-cmp/cmp"
pkgerrors "github.com/pkg/errors"
"github.com/virtual-kubelet/virtual-kubelet/internal/podutils"
"github.com/virtual-kubelet/virtual-kubelet/internal/queue"
"github.com/virtual-kubelet/virtual-kubelet/log"
"github.com/virtual-kubelet/virtual-kubelet/trace"
corev1 "k8s.io/api/core/v1"
@@ -68,11 +69,13 @@ func (pc *PodController) createOrUpdatePod(ctx context.Context, pod *corev1.Pod)
"namespace": pod.GetNamespace(),
})
// We do this so we don't mutate the pod from the informer cache
pod = pod.DeepCopy()
if err := podutils.PopulateEnvironmentVariables(ctx, pod, pc.resourceManager, pc.recorder); err != nil {
span.SetStatus(err)
return err
if !pc.skipDownwardAPIResolution {
// We do this so we don't mutate the pod from the informer cache
pod = pod.DeepCopy()
if err := podutils.PopulateEnvironmentVariables(ctx, pod, pc.resourceManager, pc.recorder); err != nil {
span.SetStatus(err)
return err
}
}
// We have to use a different pod that we pass to the provider than the one that gets used in handleProviderError
@@ -121,10 +124,11 @@ func podsEqual(pod1, pod2 *corev1.Pod) bool {
return cmp.Equal(pod1.Spec.Containers, pod2.Spec.Containers) &&
cmp.Equal(pod1.Spec.InitContainers, pod2.Spec.InitContainers) &&
cmp.Equal(pod1.Spec.EphemeralContainers, pod2.Spec.EphemeralContainers) &&
cmp.Equal(pod1.Spec.ActiveDeadlineSeconds, pod2.Spec.ActiveDeadlineSeconds) &&
cmp.Equal(pod1.Spec.Tolerations, pod2.Spec.Tolerations) &&
cmp.Equal(pod1.ObjectMeta.Labels, pod2.Labels) &&
cmp.Equal(pod1.ObjectMeta.Annotations, pod2.Annotations)
cmp.Equal(pod1.Labels, pod2.Labels) &&
cmp.Equal(pod1.Annotations, pod2.Annotations)
}
@@ -138,7 +142,7 @@ func deleteGraceTimeEqual(old, new *int64) bool {
return false
}
// podShouldEnqueue checks if two pods equal according according to podsEqual func and DeleteTimeStamp
// podShouldEnqueue checks if two pods equal according to podsEqual func and DeleteTimeStamp
func podShouldEnqueue(oldPod, newPod *corev1.Pod) bool {
if !podsEqual(oldPod, newPod) {
return true
@@ -277,7 +281,7 @@ func (pc *PodController) enqueuePodStatusUpdate(ctx context.Context, pod *corev1
ctx = span.WithField(ctx, "key", key)
var obj interface{}
err = wait.PollImmediateUntil(notificationRetryPeriod, func() (bool, error) {
err = wait.PollUntilContextCancel(ctx, notificationRetryPeriod, true, func(ctx context.Context) (bool, error) {
var ok bool
obj, ok = pc.knownPods.Load(key)
if ok {
@@ -303,14 +307,14 @@ func (pc *PodController) enqueuePodStatusUpdate(ctx context.Context, pod *corev1
// that we're in some kind of startup synchronization issue where the provider knows about a pod (as it performs
// recover, that we do not yet know about).
return false, nil
}, ctx.Done())
})
if err != nil {
if errors.IsNotFound(err) {
err = fmt.Errorf("Pod %q not found in pod lister: %w", key, err)
log.G(ctx).WithError(err).Debug("Not enqueuing pod status update")
err = fmt.Errorf("pod %q not found in pod lister: %w", key, err)
log.G(ctx).WithError(err).Debug("not enqueuing pod status update")
} else {
log.G(ctx).WithError(err).Warn("Not enqueuing pod status update due to error from pod lister")
log.G(ctx).WithError(err).Warn("not enqueuing pod status update due to error from pod lister")
}
span.SetStatus(err)
return

View File

@@ -17,6 +17,7 @@ package node
import (
"context"
"fmt"
"reflect"
"testing"
"time"
@@ -56,20 +57,20 @@ func newTestController() *TestController {
ConfigMapInformer: iFactory.Core().V1().ConfigMaps(),
SecretInformer: iFactory.Core().V1().Secrets(),
ServiceInformer: iFactory.Core().V1().Services(),
SyncPodsFromKubernetesRateLimiter: workqueue.NewMaxOfRateLimiter(
SyncPodsFromKubernetesRateLimiter: workqueue.NewTypedMaxOfRateLimiter[any](
// The default upper bound is 1000 seconds. Let's not use that.
workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 10*time.Millisecond),
&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
workqueue.NewTypedItemExponentialFailureRateLimiter[any](5*time.Millisecond, 10*time.Millisecond),
&workqueue.TypedBucketRateLimiter[any]{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
),
SyncPodStatusFromProviderRateLimiter: workqueue.NewMaxOfRateLimiter(
SyncPodStatusFromProviderRateLimiter: workqueue.NewTypedMaxOfRateLimiter[any](
// The default upper bound is 1000 seconds. Let's not use that.
workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 10*time.Millisecond),
&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
workqueue.NewTypedItemExponentialFailureRateLimiter[any](5*time.Millisecond, 10*time.Millisecond),
&workqueue.TypedBucketRateLimiter[any]{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
),
DeletePodsFromKubernetesRateLimiter: workqueue.NewMaxOfRateLimiter(
DeletePodsFromKubernetesRateLimiter: workqueue.NewTypedMaxOfRateLimiter[any](
// The default upper bound is 1000 seconds. Let's not use that.
workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 10*time.Millisecond),
&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
workqueue.NewTypedItemExponentialFailureRateLimiter[any](5*time.Millisecond, 10*time.Millisecond),
&workqueue.TypedBucketRateLimiter[any]{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
),
})
@@ -211,8 +212,8 @@ func TestPodCreateNewPod(t *testing.T) {
svr := newTestController()
pod := &corev1.Pod{}
pod.ObjectMeta.Namespace = "default" //nolint:goconst
pod.ObjectMeta.Name = "nginx" //nolint:goconst
pod.Namespace = "default" //nolint:goconst
pod.Name = "nginx" //nolint:goconst
pod.Spec = newPodSpec()
err := svr.createOrUpdatePod(context.Background(), pod.DeepCopy())
@@ -223,12 +224,48 @@ func TestPodCreateNewPod(t *testing.T) {
assert.Check(t, is.Equal(svr.mock.updates.read(), 0))
}
func TestPodCreateNewPodWithNoDownwardAPIResolution(t *testing.T) {
svr := newTestController()
svr.skipDownwardAPIResolution = true
pod := &corev1.Pod{}
pod.Namespace = "default" //nolint:goconst
pod.Name = "nginx" //nolint:goconst
pod.Spec = newPodSpec()
pod.Spec.Containers[0].Env = []corev1.EnvVar{
{
Name: "MY_NODE_NAME",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "spec.nodeName",
},
},
},
}
err := svr.createOrUpdatePod(context.Background(), pod.DeepCopy())
assert.Check(t, is.Nil(err))
// createOrUpdate called CreatePod but did not call UpdatePod because the pod did not exist
assert.Check(t, is.Equal(svr.mock.creates.read(), 1))
assert.Check(t, is.Equal(svr.mock.updates.read(), 0))
// make sure the downward API field in env was left alone
key, err := buildKey(pod)
assert.Check(t, is.Nil(err))
createdPod, ok := svr.mock.pods.Load(key)
assert.Check(t, ok)
// createdPod went through the pod controller logic, make sure the downward API wasn't resolved
assert.Check(t, is.DeepEqual(createdPod.(*corev1.Pod).Spec.Containers[0].Env, pod.Spec.Containers[0].Env))
}
func TestPodUpdateExisting(t *testing.T) {
svr := newTestController()
pod := &corev1.Pod{}
pod.ObjectMeta.Namespace = "default"
pod.ObjectMeta.Name = "nginx"
pod.Namespace = "default"
pod.Name = "nginx"
pod.Spec = newPodSpec()
err := svr.createOrUpdatePod(context.Background(), pod.DeepCopy())
@@ -251,8 +288,8 @@ func TestPodNoSpecChange(t *testing.T) {
svr := newTestController()
pod := &corev1.Pod{}
pod.ObjectMeta.Namespace = "default"
pod.ObjectMeta.Name = "nginx"
pod.Namespace = "default"
pod.Name = "nginx"
pod.Spec = newPodSpec()
err := svr.createOrUpdatePod(context.Background(), pod.DeepCopy())
@@ -272,8 +309,8 @@ func TestPodStatusDelete(t *testing.T) {
ctx := context.Background()
c := newTestController()
pod := &corev1.Pod{}
pod.ObjectMeta.Namespace = "default"
pod.ObjectMeta.Name = "nginx"
pod.Namespace = "default"
pod.Name = "nginx"
pod.Spec = newPodSpec()
fk8s := fake.NewSimpleClientset(pod)
c.client = fk8s
@@ -293,7 +330,8 @@ func TestPodStatusDelete(t *testing.T) {
if err != nil && !errors.IsNotFound(err) {
t.Fatalf("Get pod %v failed", key)
}
if newPod != nil && newPod.DeletionTimestamp == nil {
if newPod != nil && !reflect.ValueOf(*newPod).IsZero() && newPod.DeletionTimestamp == nil {
t.Fatalf("Pod %v delete failed", key)
}
t.Logf("pod delete success")
@@ -337,8 +375,8 @@ func TestReCreatePodRace(t *testing.T) {
ctx := context.Background()
c := newTestController()
pod := &corev1.Pod{}
pod.ObjectMeta.Namespace = "default"
pod.ObjectMeta.Name = "nginx"
pod.Namespace = "default"
pod.Name = "nginx"
pod.Spec = newPodSpec()
pod.UID = "aaaaa"
podCopy := pod.DeepCopy()
@@ -360,7 +398,7 @@ func TestReCreatePodRace(t *testing.T) {
return true, nil, errors.NewConflict(schema.GroupResource{Group: "", Resource: "pods"}, "nginx", fmt.Errorf("test conflict"))
})
c.client.AddReactor("get", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
name := action.(core.DeleteAction).GetName()
name := action.(core.GetAction).GetName()
t.Logf("get pod %s", name)
return true, podCopy, nil
})
@@ -394,7 +432,7 @@ func TestReCreatePodRace(t *testing.T) {
})
c.client.AddReactor("get", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
name := action.(core.DeleteAction).GetName()
name := action.(core.GetAction).GetName()
t.Logf("get pod %s", name)
return true, nil, errors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "nginx")
})
@@ -430,7 +468,7 @@ func TestReCreatePodRace(t *testing.T) {
})
c.client.AddReactor("get", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
name := action.(core.DeleteAction).GetName()
name := action.(core.GetAction).GetName()
t.Logf("get pod %s", name)
return true, nil, errors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "nginx")
})

View File

@@ -20,12 +20,11 @@ import (
"sync"
"time"
"github.com/virtual-kubelet/virtual-kubelet/internal/queue"
"github.com/google/go-cmp/cmp"
pkgerrors "github.com/pkg/errors"
"github.com/virtual-kubelet/virtual-kubelet/errdefs"
"github.com/virtual-kubelet/virtual-kubelet/internal/manager"
"github.com/virtual-kubelet/virtual-kubelet/internal/queue"
"github.com/virtual-kubelet/virtual-kubelet/log"
"github.com/virtual-kubelet/virtual-kubelet/trace"
corev1 "k8s.io/api/core/v1"
@@ -141,6 +140,11 @@ type PodController struct {
// This is used since `pc.Run()` is typically called in a goroutine and managing
// this can be non-trivial for callers.
err error
// skipDownwardAPIResolution can be used to skip any attempts at resolving downward API references
// in pods before calling CreatePod on the provider.
// Providers need this if they need to do their own custom resolving
skipDownwardAPIResolution bool
}
type knownPod struct {
@@ -177,11 +181,19 @@ type PodControllerConfig struct {
ServiceInformer corev1informers.ServiceInformer
// SyncPodsFromKubernetesRateLimiter defines the rate limit for the SyncPodsFromKubernetes queue
SyncPodsFromKubernetesRateLimiter workqueue.RateLimiter
SyncPodsFromKubernetesRateLimiter workqueue.TypedRateLimiter[any]
// SyncPodsFromKubernetesShouldRetryFunc allows for a custom retry policy for the SyncPodsFromKubernetes queue
SyncPodsFromKubernetesShouldRetryFunc ShouldRetryFunc
// DeletePodsFromKubernetesRateLimiter defines the rate limit for the DeletePodsFromKubernetesRateLimiter queue
DeletePodsFromKubernetesRateLimiter workqueue.RateLimiter
DeletePodsFromKubernetesRateLimiter workqueue.TypedRateLimiter[any]
// DeletePodsFromKubernetesShouldRetryFunc allows for a custom retry policy for the SyncPodsFromKubernetes queue
DeletePodsFromKubernetesShouldRetryFunc ShouldRetryFunc
// SyncPodStatusFromProviderRateLimiter defines the rate limit for the SyncPodStatusFromProviderRateLimiter queue
SyncPodStatusFromProviderRateLimiter workqueue.RateLimiter
SyncPodStatusFromProviderRateLimiter workqueue.TypedRateLimiter[any]
// SyncPodStatusFromProviderShouldRetryFunc allows for a custom retry policy for the SyncPodStatusFromProvider queue
SyncPodStatusFromProviderShouldRetryFunc ShouldRetryFunc
// Add custom filtering for pod informer event handlers
// Use this for cases where the pod informer handles more than pods assigned to this node
@@ -189,6 +201,11 @@ type PodControllerConfig struct {
// For example, if the pod informer is not filtering based on pod.Spec.NodeName, you should
// set that filter here so the pod controller does not handle events for pods assigned to other nodes.
PodEventFilterFunc PodEventFilterFunc
// SkipDownwardAPIResolution can be used to skip any attempts at resolving downward API references
// in pods before calling CreatePod on the provider.
// Providers need this if they need to do their own custom resolving
SkipDownwardAPIResolution bool
}
// NewPodController creates a new pod controller with the provided config.
@@ -215,13 +232,13 @@ func NewPodController(cfg PodControllerConfig) (*PodController, error) {
return nil, errdefs.InvalidInput("missing provider")
}
if cfg.SyncPodsFromKubernetesRateLimiter == nil {
cfg.SyncPodsFromKubernetesRateLimiter = workqueue.DefaultControllerRateLimiter()
cfg.SyncPodsFromKubernetesRateLimiter = workqueue.DefaultTypedControllerRateLimiter[any]()
}
if cfg.DeletePodsFromKubernetesRateLimiter == nil {
cfg.DeletePodsFromKubernetesRateLimiter = workqueue.DefaultControllerRateLimiter()
cfg.DeletePodsFromKubernetesRateLimiter = workqueue.DefaultTypedControllerRateLimiter[any]()
}
if cfg.SyncPodStatusFromProviderRateLimiter == nil {
cfg.SyncPodStatusFromProviderRateLimiter = workqueue.DefaultControllerRateLimiter()
cfg.SyncPodStatusFromProviderRateLimiter = workqueue.DefaultTypedControllerRateLimiter[any]()
}
rm, err := manager.NewResourceManager(cfg.PodInformer.Lister(), cfg.SecretInformer.Lister(), cfg.ConfigMapInformer.Lister(), cfg.ServiceInformer.Lister())
if err != nil {
@@ -229,20 +246,21 @@ func NewPodController(cfg PodControllerConfig) (*PodController, error) {
}
pc := &PodController{
client: cfg.PodClient,
podsInformer: cfg.PodInformer,
podsLister: cfg.PodInformer.Lister(),
provider: cfg.Provider,
resourceManager: rm,
ready: make(chan struct{}),
done: make(chan struct{}),
recorder: cfg.EventRecorder,
podEventFilterFunc: cfg.PodEventFilterFunc,
client: cfg.PodClient,
podsInformer: cfg.PodInformer,
podsLister: cfg.PodInformer.Lister(),
provider: cfg.Provider,
resourceManager: rm,
ready: make(chan struct{}),
done: make(chan struct{}),
recorder: cfg.EventRecorder,
podEventFilterFunc: cfg.PodEventFilterFunc,
skipDownwardAPIResolution: cfg.SkipDownwardAPIResolution,
}
pc.syncPodsFromKubernetes = queue.New(cfg.SyncPodsFromKubernetesRateLimiter, "syncPodsFromKubernetes", pc.syncPodFromKubernetesHandler)
pc.deletePodsFromKubernetes = queue.New(cfg.DeletePodsFromKubernetesRateLimiter, "deletePodsFromKubernetes", pc.deletePodsFromKubernetesHandler)
pc.syncPodStatusFromProvider = queue.New(cfg.SyncPodStatusFromProviderRateLimiter, "syncPodStatusFromProvider", pc.syncPodStatusFromProviderHandler)
pc.syncPodsFromKubernetes = queue.New(cfg.SyncPodsFromKubernetesRateLimiter, "syncPodsFromKubernetes", pc.syncPodFromKubernetesHandler, cfg.SyncPodsFromKubernetesShouldRetryFunc)
pc.deletePodsFromKubernetes = queue.New(cfg.DeletePodsFromKubernetesRateLimiter, "deletePodsFromKubernetes", pc.deletePodsFromKubernetesHandler, cfg.DeletePodsFromKubernetesShouldRetryFunc)
pc.syncPodStatusFromProvider = queue.New(cfg.SyncPodStatusFromProviderRateLimiter, "syncPodStatusFromProvider", pc.syncPodStatusFromProviderHandler, cfg.SyncPodStatusFromProviderShouldRetryFunc)
return pc, nil
}
@@ -395,7 +413,10 @@ func (pc *PodController) Run(ctx context.Context, podSyncWorkers int) (retErr er
}
}
pc.podsInformer.Informer().AddEventHandler(eventHandler)
_, err := pc.podsInformer.Informer().AddEventHandler(eventHandler)
if err != nil {
log.G(ctx).Error(err)
}
// Perform a reconciliation step that deletes any dangling pods from the provider.
// This happens only when the virtual-kubelet is starting, and operates on a "best-effort" basis.
@@ -443,6 +464,51 @@ func (pc *PodController) Err() error {
return pc.err
}
// SyncPodsFromKubernetesQueueLen returns the length of the SyncPodsFromKubernetes queue, which include items being processed and unprocessed
func (pc *PodController) SyncPodsFromKubernetesQueueLen() int {
return pc.syncPodsFromKubernetes.Len()
}
// SyncPodsFromKubernetesQueueUnprocessedLen returns the length of the unprocessed items in the SyncPodsFromKubernetes queue
func (pc *PodController) SyncPodsFromKubernetesQueueUnprocessedLen() int {
return pc.syncPodsFromKubernetes.UnprocessedLen()
}
// SyncPodsFromKubernetesQueueItemsBeingProcessedLen returns the length of the items being processed in the SyncPodsFromKubernetes queue
func (pc *PodController) SyncPodsFromKubernetesQueueItemsBeingProcessedLen() int {
return pc.syncPodsFromKubernetes.ItemsBeingProcessedLen()
}
// DeletePodsFromKubernetesQueueLen returns the length of the DeletePodsFromKubernetes queue, which include items being processed and unprocessed
func (pc *PodController) DeletePodsFromKubernetesQueueLen() int {
return pc.deletePodsFromKubernetes.Len()
}
// DeletePodsFromKubernetesQueueUnprocessedLen returns the length of the unprocessed items in the DeletePodsFromKubernetes queue
func (pc *PodController) DeletePodsFromKubernetesQueueUnprocessedLen() int {
return pc.deletePodsFromKubernetes.UnprocessedLen()
}
// DeletePodsFromKubernetesQueueItemsBeingProcessedLen returns the length of the items being processed in the DeletePodsFromKubernetes queue
func (pc *PodController) DeletePodsFromKubernetesQueueItemsBeingProcessedLen() int {
return pc.deletePodsFromKubernetes.ItemsBeingProcessedLen()
}
// SyncPodStatusFromProviderQueueLen returns the length of the SyncPodStatusFromProvider queue, which include items being processed and unprocessed
func (pc *PodController) SyncPodStatusFromProviderQueueLen() int {
return pc.syncPodStatusFromProvider.Len()
}
// SyncPodStatusFromProviderQueueUnprocessedLen returns the length of the unprocessed items in the SyncPodStatusFromProvider queue
func (pc *PodController) SyncPodStatusFromProviderQueueUnprocessedLen() int {
return pc.syncPodStatusFromProvider.UnprocessedLen()
}
// SyncPodStatusFromProviderQueueItemsBeingProcessedLen returns the length of the items being processed in the SyncPodStatusFromProvider queue
func (pc *PodController) SyncPodStatusFromProviderQueueItemsBeingProcessedLen() int {
return pc.syncPodStatusFromProvider.ItemsBeingProcessedLen()
}
// syncPodFromKubernetesHandler compares the actual state with the desired, and attempts to converge the two.
func (pc *PodController) syncPodFromKubernetesHandler(ctx context.Context, key string) error {
ctx, span := trace.StartSpan(ctx, "syncPodFromKubernetesHandler")
@@ -509,7 +575,6 @@ func (pc *PodController) syncPodInProvider(ctx context.Context, pod *corev1.Pod,
// more context is here: https://github.com/virtual-kubelet/virtual-kubelet/pull/760
if pod.DeletionTimestamp != nil && !running(&pod.Status) {
log.G(ctx).Debug("Force deleting pod from API Server as it is no longer running")
pc.deletePodsFromKubernetes.EnqueueWithoutRateLimit(ctx, key)
key = fmt.Sprintf("%v/%v", key, pod.UID)
pc.deletePodsFromKubernetes.EnqueueWithoutRateLimit(ctx, key)
return nil

View File

@@ -123,8 +123,8 @@ func TestPodEventFilter(t *testing.T) {
}
pod := &corev1.Pod{}
pod.ObjectMeta.Namespace = "default"
pod.ObjectMeta.Name = "nginx"
pod.Namespace = "default"
pod.Name = "nginx"
pod.Spec = newPodSpec()
podC := tc.client.CoreV1().Pods(testNamespace)

34
node/queue.go Normal file
View File

@@ -0,0 +1,34 @@
package node
import (
"github.com/virtual-kubelet/virtual-kubelet/internal/queue"
)
// These are exportable definitions of the queue package:
// ShouldRetryFunc is a mechanism to have a custom retry policy
//
// it is passed metadata about the work item when the handler returns an error. It returns the following:
// * The key
// * The number of attempts that this item has already had (and failed)
// * The (potentially wrapped) error from the queue handler.
//
// The return value is an error, and optionally an amount to delay the work.
// If an error is returned, the work will be aborted, and the returned error is bubbled up. It can be the error that
// was passed in or that error can be wrapped.
//
// If the work item should be is to be retried, a delay duration may be specified. The delay is used to schedule when
// the item should begin processing relative to now, it does not necessarily dictate when the item will start work.
// Items are processed in the order they are scheduled. If the delay is nil, it will fall back to the default behaviour
// of the queue, and use the rate limiter that's configured to determine when to start work.
//
// If the delay is negative, the item will be scheduled "earlier" than now. This will result in the item being executed
// earlier than other items in the FIFO work order.
type ShouldRetryFunc = queue.ShouldRetryFunc
// DefaultRetryFunc is the default function used for retries by the queue subsystem. Its only policy is that it gives up
// after MaxRetries, and falls back to the rate limiter for all other retries.
var DefaultRetryFunc = queue.DefaultRetryFunc
// MaxRetries is the number of times we try to process a given key before permanently forgetting it.
var MaxRetries = queue.MaxRetries

View File

@@ -134,7 +134,12 @@ func (p *syncProviderWrapper) syncPodStatuses(ctx context.Context) {
for _, pod := range pods {
if shouldSkipPodStatusUpdate(pod) {
log.G(ctx).Debug("Skipping pod status update")
log.G(ctx).WithFields(log.Fields{
"pod": pod.Name,
"namespace": pod.Namespace,
"phase": pod.Status.Phase,
"status": pod.Status.Reason,
}).Debug("Skipping pod status update")
continue
}
@@ -153,7 +158,7 @@ func (p *syncProviderWrapper) updatePodStatus(ctx context.Context, podFromKubern
ctx = addPodAttributes(ctx, span, podFromKubernetes)
var statusErr error
podStatus, err := p.PodLifecycleHandler.GetPodStatus(ctx, podFromKubernetes.Namespace, podFromKubernetes.Name)
podStatus, err := p.GetPodStatus(ctx, podFromKubernetes.Namespace, podFromKubernetes.Name)
if err != nil {
if !errdefs.IsNotFound(err) {
span.SetStatus(err)
@@ -179,7 +184,7 @@ func (p *syncProviderWrapper) updatePodStatus(ctx context.Context, podFromKubern
return nil
}
if podFromKubernetes.Status.Phase != corev1.PodRunning && time.Since(podFromKubernetes.ObjectMeta.CreationTimestamp.Time) <= time.Minute {
if podFromKubernetes.Status.Phase != corev1.PodRunning && time.Since(podFromKubernetes.CreationTimestamp.Time) <= time.Minute {
span.SetStatus(statusErr)
return statusErr
}

View File

@@ -1,16 +1,18 @@
package e2e
import (
"bytes"
"context"
"fmt"
"testing"
"time"
"github.com/prometheus/common/expfmt"
"github.com/virtual-kubelet/virtual-kubelet/internal/podutils"
"gotest.tools/assert"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
)
const (
@@ -111,6 +113,72 @@ func (ts *EndToEndTestSuite) TestGetStatsSummary(t *testing.T) {
}
}
// TestGetMetricsResource creates a pod having two containers and queries the /metrics/resource endpoint of the virtual-kubelet.
// It expects this endpoint to return stats for the current node, as well as for the aforementioned pod and each of its two containers.
func (ts *EndToEndTestSuite) TestGetMetricsResource(t *testing.T) {
ctx := context.Background()
// Create a pod with prefix "nginx-" having three containers.
pod, err := f.CreatePod(ctx, f.CreateDummyPodObjectWithPrefix(t.Name(), "nginx", "foo", "bar", "baz"))
if err != nil {
t.Fatal(err)
}
// Delete the "nginx-0-X" pod after the test finishes.
defer func() {
if err := f.DeletePodImmediately(ctx, pod.Namespace, pod.Name); err != nil && !apierrors.IsNotFound(err) {
t.Error(err)
}
}()
// Wait for the "nginx-" pod to be reported as running and ready.
if _, err := f.WaitUntilPodReady(pod.Namespace, pod.Name); err != nil {
t.Fatal(err)
}
// Grab the stats from the provider.
metricsResourceResponse, err := f.GetMetricsResource(ctx)
if err != nil {
t.Fatal(err)
}
// decode metrics response bytes to metric family
reader := bytes.NewReader(metricsResourceResponse)
parser := expfmt.TextParser{}
metricsFamilyMap, err := parser.TextToMetricFamilies(reader)
if err != nil {
t.Fatal(err)
}
// Make sure the "nginx-" pod exists in the metrics returned.
currentContainerStatsCount := 0
found := false
for metricName, metricFamily := range metricsFamilyMap {
if metricName == "pod_cpu_usage_seconds_total" {
for _, metric := range metricFamily.Metric {
if *metric.Label[1].Value == pod.Name {
found = true
}
}
}
if metricName == "container_cpu_usage_seconds_total" {
for _, metric := range metricFamily.Metric {
if *metric.Label[1].Value == pod.Name {
currentContainerStatsCount += 1
}
}
}
}
if !found {
t.Fatalf("Pod %s not found in metrics", pod.Name)
}
// Make sure that we've got stats for all the containers in the "nginx-" pod.
desiredContainerStatsCount := len(pod.Spec.Containers)
if currentContainerStatsCount != desiredContainerStatsCount {
t.Fatalf("expected stats for %d containers, got stats for %d containers", desiredContainerStatsCount, currentContainerStatsCount)
}
}
// TestPodLifecycleGracefulDelete creates a pod and verifies that the provider has been asked to create it.
// Then, it deletes the pods and verifies that the provider has been asked to delete it.
// These verifications are made using the /stats/summary endpoint of the virtual-kubelet, by checking for the presence or absence of the pods.
@@ -183,8 +251,8 @@ func (ts *EndToEndTestSuite) TestPodLifecycleGracefulDelete(t *testing.T) {
// Make sure we saw the delete event, and the delete event was graceful
assert.Assert(t, podLast != nil)
assert.Assert(t, podLast.ObjectMeta.GetDeletionGracePeriodSeconds() != nil)
assert.Assert(t, *podLast.ObjectMeta.GetDeletionGracePeriodSeconds() > 0)
assert.Assert(t, podLast.GetDeletionGracePeriodSeconds() != nil)
assert.Assert(t, *podLast.GetDeletionGracePeriodSeconds() > 0)
}
// TestPodLifecycleForceDelete creates one podsand verifies that the provider has created them
@@ -401,9 +469,9 @@ func (ts *EndToEndTestSuite) TestCreatePodWithMandatoryInexistentConfigMap(t *te
// findPodInPodStats returns the index of the specified pod in the .pods field of the specified Summary object.
// It returns an error if the specified pod is not found.
func findPodInPodStats(summary *v1alpha1.Summary, pod *v1.Pod) (int, error) {
func findPodInPodStats(summary *stats.Summary, pod *v1.Pod) (int, error) {
for i, p := range summary.Pods {
if p.PodRef.Namespace == pod.Namespace && p.PodRef.Name == pod.Name && string(p.PodRef.UID) == string(pod.UID) {
if p.PodRef.Namespace == pod.Namespace && p.PodRef.Name == pod.Name && p.PodRef.UID == string(pod.UID) {
return i, nil
}
}

View File

@@ -1,61 +0,0 @@
package e2e
import (
"context"
"testing"
"time"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
watchapi "k8s.io/apimachinery/pkg/watch"
)
// TestNodeCreateAfterDelete makes sure that a node is automatically recreated
// if it is deleted while VK is running.
func (ts *EndToEndTestSuite) TestNodeCreateAfterDelete(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
podList, err := f.KubeClient.CoreV1().Pods(f.Namespace).List(ctx, metav1.ListOptions{
FieldSelector: fields.OneTermEqualSelector("spec.nodeName", f.NodeName).String(),
})
assert.NilError(t, err)
assert.Assert(t, is.Len(podList.Items, 0), "Kubernetes does not allow node deletion with dependent objects (pods) in existence: %v")
chErr := make(chan error, 1)
originalNode, err := f.GetNode(ctx)
assert.NilError(t, err)
ctx, cancel = context.WithTimeout(ctx, time.Minute)
defer cancel()
go func() {
wait := func(e watchapi.Event) (bool, error) {
err = ctx.Err()
// Our timeout has expired
if err != nil {
return true, err
}
if e.Type == watchapi.Deleted || e.Type == watchapi.Error {
return false, nil
}
return originalNode.ObjectMeta.UID != e.Object.(*v1.Node).ObjectMeta.UID, nil
}
chErr <- f.WaitUntilNodeCondition(wait)
}()
assert.NilError(t, f.DeleteNode(ctx))
select {
case result := <-chErr:
assert.NilError(t, result, "Did not observe new node object created after deletion")
case <-ctx.Done():
t.Fatal("Test timed out while waiting for node object to be deleted / recreated")
}
}

View File

@@ -79,8 +79,12 @@ func (s *span) SetStatus(err error) {
status.Code = octrace.StatusCodeNotFound
case errdefs.IsInvalidInput(err):
status.Code = octrace.StatusCodeInvalidArgument
// TODO: other error types
case errdefs.IsForbidden(err):
status.Code = octrace.StatusCodePermissionDenied
case errdefs.IsUnauthorized(err):
status.Code = octrace.StatusCodeUnauthenticated
default:
// TODO: other error types
status.Code = octrace.StatusCodeUnknown
}

View File

@@ -0,0 +1,266 @@
// Copyright © 2022 The virtual-kubelet authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package opentelemetry implements a github.com/virtual-kubelet/virtual-kubelet/trace.Tracer
// using openTelemetry as a backend.
//
// Use this by setting `trace.T = Adapter{}`
//
// For customizing trace provider used in Adapter, set trace provider by
// `otel.SetTracerProvider(*sdktrace.TracerProvider)`. Examples of customize are setting service name,
// use your own exporter (e.g. jaeger, otlp, prometheus, zipkin, and stdout) etc. Do not forget
// to call TracerProvider.Shutdown() when you create your TracerProvider to avoid memory leak.
package opentelemetry
import (
"context"
"fmt"
"sync"
"time"
"go.opentelemetry.io/otel/codes"
"github.com/virtual-kubelet/virtual-kubelet/log"
"github.com/virtual-kubelet/virtual-kubelet/trace"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
ot "go.opentelemetry.io/otel/trace"
)
type logLevel string
const (
lDebug logLevel = "DEBUG"
lInfo logLevel = "INFO"
lWarn logLevel = "WARN"
lErr logLevel = "ERROR"
lFatal logLevel = "FATAL"
)
// Adapter implements the trace.Tracer interface for openTelemetry
type Adapter struct{}
// StartSpan creates a new span from openTelemetry using the given name.
func (Adapter) StartSpan(ctx context.Context, name string) (context.Context, trace.Span) {
ctx, ots := otel.Tracer(name).Start(ctx, name)
l := log.G(ctx).WithField("method", name)
s := &span{s: ots, l: l}
ctx = log.WithLogger(ctx, s.Logger())
return ctx, s
}
type span struct {
mu sync.Mutex
s ot.Span
l log.Logger
}
func (s *span) End() {
s.s.End()
}
func (s *span) SetStatus(err error) {
if !s.s.IsRecording() {
return
}
if err == nil {
s.s.SetStatus(codes.Ok, "")
} else {
s.s.SetStatus(codes.Error, err.Error())
}
}
func (s *span) WithField(ctx context.Context, key string, val interface{}) context.Context {
s.mu.Lock()
s.l = s.l.WithField(key, val)
ctx = log.WithLogger(ctx, &logger{s: s.s, l: s.l})
s.mu.Unlock()
if s.s.IsRecording() {
s.s.SetAttributes(makeAttribute(key, val))
}
return ctx
}
func (s *span) WithFields(ctx context.Context, f log.Fields) context.Context {
s.mu.Lock()
s.l = s.l.WithFields(f)
ctx = log.WithLogger(ctx, &logger{s: s.s, l: s.l})
s.mu.Unlock()
if s.s.IsRecording() {
attrs := make([]attribute.KeyValue, 0, len(f))
for k, v := range f {
attrs = append(attrs, makeAttribute(k, v))
}
s.s.SetAttributes(attrs...)
}
return ctx
}
func (s *span) Logger() log.Logger {
return &logger{s: s.s, l: s.l}
}
type logger struct {
s ot.Span
l log.Logger
a []attribute.KeyValue
}
func (l *logger) Debug(args ...interface{}) {
l.logEvent(lDebug, args...)
}
func (l *logger) Debugf(f string, args ...interface{}) {
l.logEventf(lDebug, f, args...)
}
func (l *logger) Info(args ...interface{}) {
l.logEvent(lInfo, args...)
}
func (l *logger) Infof(f string, args ...interface{}) {
l.logEventf(lInfo, f, args...)
}
func (l *logger) Warn(args ...interface{}) {
l.logEvent(lWarn, args...)
}
func (l *logger) Warnf(f string, args ...interface{}) {
l.logEventf(lWarn, f, args...)
}
func (l *logger) Error(args ...interface{}) {
l.logEvent(lErr, args...)
}
func (l *logger) Errorf(f string, args ...interface{}) {
l.logEventf(lErr, f, args...)
}
func (l *logger) Fatal(args ...interface{}) {
l.logEvent(lFatal, args...)
}
func (l *logger) Fatalf(f string, args ...interface{}) {
l.logEventf(lFatal, f, args...)
}
func (l *logger) logEvent(ll logLevel, args ...interface{}) {
msg := fmt.Sprint(args...)
switch ll {
case lDebug:
l.l.Debug(msg)
case lInfo:
l.l.Info(msg)
case lWarn:
l.l.Warn(msg)
case lErr:
l.l.Error(msg)
case lFatal:
l.l.Fatal(msg)
}
if !l.s.IsRecording() {
return
}
l.s.AddEvent(msg, ot.WithTimestamp(time.Now()))
}
func (l *logger) logEventf(ll logLevel, f string, args ...interface{}) {
switch ll {
case lDebug:
l.l.Debugf(f, args...)
case lInfo:
l.l.Infof(f, args...)
case lWarn:
l.l.Warnf(f, args...)
case lErr:
l.l.Errorf(f, args...)
case lFatal:
l.l.Fatalf(f, args...)
}
if !l.s.IsRecording() {
return
}
msg := fmt.Sprintf(f, args...)
l.s.AddEvent(msg, ot.WithTimestamp(time.Now()))
}
func (l *logger) WithError(err error) log.Logger {
return l.WithField("err", err)
}
func (l *logger) WithField(k string, value interface{}) log.Logger {
var attrs []attribute.KeyValue
if l.s.IsRecording() {
attrs = make([]attribute.KeyValue, len(l.a)+1)
copy(attrs, l.a)
attrs[len(attrs)-1] = makeAttribute(k, value)
}
return &logger{s: l.s, a: attrs, l: l.l.WithField(k, value)}
}
func (l *logger) WithFields(fields log.Fields) log.Logger {
var attrs []attribute.KeyValue
if l.s.IsRecording() {
attrs = make([]attribute.KeyValue, len(l.a), len(l.a)+len(fields))
copy(attrs, l.a)
for k, v := range fields {
attrs = append(attrs, makeAttribute(k, v))
}
}
return &logger{s: l.s, a: attrs, l: l.l.WithFields(fields)}
}
func makeAttribute(key string, val interface{}) (attr attribute.KeyValue) {
switch v := val.(type) {
case string:
return attribute.String(key, v)
// case []string:
// return attribute.StringSlice(key, v)
case fmt.Stringer:
return attribute.Stringer(key, v)
case int:
return attribute.Int(key, v)
// case []int:
// return attribute.IntSlice(key, v)
case int64:
return attribute.Int64(key, v)
case float64:
return attribute.Float64(key, v)
// case []float64:
// return attribute.Float64Slice(key, v)
// case []int64:
// return attribute.Int64Slice(key, v)
case bool:
return attribute.Bool(key, v)
// case []bool:
// return attribute.BoolSlice(key, v)
case error:
if v == nil {
return attribute.String(key, "")
}
return attribute.String(key, v.Error())
default:
return attribute.String(key, fmt.Sprintf("%+v", val))
}
}

View File

@@ -0,0 +1,651 @@
// Copyright © 2022 The virtual-kubelet authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package opentelemetry
import (
"context"
"fmt"
"sort"
"strconv"
"sync"
"testing"
"time"
"github.com/pkg/errors"
"github.com/virtual-kubelet/virtual-kubelet/log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
"gotest.tools/assert"
"gotest.tools/assert/cmp"
)
func TestStartSpan(t *testing.T) {
t.Run("addField", func(t *testing.T) {
tearDown, p, _ := setupSuite()
defer tearDown(p)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
a := Adapter{}
_, s := a.StartSpan(ctx, "name")
s.End()
})
}
func TestSetStatus(t *testing.T) {
testCases := []struct {
description string
spanName string
inputStatus error
expectedCode codes.Code
expectedDescription string
}{
{
description: "error status",
spanName: "test",
inputStatus: errors.New("fake msg"),
expectedCode: codes.Error,
expectedDescription: "fake msg",
}, {
description: "non-error status",
spanName: "test",
inputStatus: nil,
expectedCode: codes.Ok,
expectedDescription: "",
},
}
for _, tt := range testCases {
t.Run(tt.description, func(t *testing.T) {
tearDown, p, e := setupSuite()
defer tearDown(p)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx, ots := otel.Tracer(tt.spanName).Start(ctx, tt.spanName)
l := log.G(ctx).WithField("method", tt.spanName)
s := &span{s: ots, l: l}
s.SetStatus(tt.inputStatus)
assert.Assert(t, s.s.IsRecording())
s.End()
assert.Assert(t, !s.s.IsRecording())
assert.Assert(t, e.status == tt.expectedCode)
s.SetStatus(tt.inputStatus) // should not be panic even if span is ended.
})
}
}
func TestWithField(t *testing.T) {
type field struct {
key string
value interface{}
}
testCases := []struct {
description string
spanName string
fields []field
expectedAttributes []attribute.KeyValue
}{
{
description: "single field",
spanName: "test",
fields: []field{{key: "testKey1", value: "value1"}},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}},
}, {
description: "multiple unique fields",
spanName: "test",
fields: []field{{key: "testKey1", value: "value1"}, {key: "testKey2", value: "value2"}},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}, {Key: "testKey2", Value: attribute.StringValue("value2")}},
}, {
description: "duplicated fields",
spanName: "test",
fields: []field{{key: "testKey1", value: "value1"}, {key: "testKey1", value: "value2"}},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value2")}},
},
}
for _, tt := range testCases {
t.Run(tt.description, func(t *testing.T) {
tearDown, p, e := setupSuite()
defer tearDown(p)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx, ots := otel.Tracer(tt.spanName).Start(ctx, tt.spanName)
l := log.G(ctx).WithField("method", tt.spanName)
s := &span{s: ots, l: l}
for _, f := range tt.fields {
ctx = s.WithField(ctx, f.key, f.value)
}
s.End()
assert.Assert(t, len(e.attributes) == len(tt.expectedAttributes))
for _, a := range tt.expectedAttributes {
cmp.Contains(e.attributes, a)
}
})
}
}
func TestWithFields(t *testing.T) {
testCases := []struct {
description string
spanName string
fields log.Fields
expectedAttributes []attribute.KeyValue
}{
{
description: "single field",
spanName: "test",
fields: log.Fields{"testKey1": "value1"},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}},
}, {
description: "multiple unique fields",
spanName: "test",
fields: log.Fields{"testKey1": "value1", "testKey2": "value2"},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}, {Key: "testKey2", Value: attribute.StringValue("value2")}},
},
}
for _, tt := range testCases {
t.Run(tt.description, func(t *testing.T) {
tearDown, p, e := setupSuite()
defer tearDown(p)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx, ots := otel.Tracer(tt.spanName).Start(ctx, tt.spanName)
l := log.G(ctx).WithField("method", tt.spanName)
s := &span{s: ots, l: l}
_ = s.WithFields(ctx, tt.fields)
s.End()
assert.Assert(t, len(e.attributes) == len(tt.expectedAttributes))
for _, a := range tt.expectedAttributes {
cmp.Contains(e.attributes, a)
}
})
}
}
func TestLog(t *testing.T) {
testCases := []struct {
description string
spanName string
logLevel logLevel
fields log.Fields
msg string
expectedEvents []sdktrace.Event
expectedAttributes []attribute.KeyValue
}{
{
description: "debug",
spanName: "test",
logLevel: lDebug,
fields: log.Fields{"testKey1": "value1"},
msg: "message",
expectedEvents: []sdktrace.Event{{Name: "message"}},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}},
}, {
description: "info",
spanName: "test",
logLevel: lInfo,
fields: log.Fields{"testKey1": "value1"},
msg: "message",
expectedEvents: []sdktrace.Event{{Name: "message"}},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}},
}, {
description: "warn",
spanName: "test",
logLevel: lWarn,
fields: log.Fields{"testKey1": "value1"},
msg: "message",
expectedEvents: []sdktrace.Event{{Name: "message"}},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}},
}, {
description: "error",
spanName: "test",
logLevel: lErr,
fields: log.Fields{"testKey1": "value1"},
msg: "message",
expectedEvents: []sdktrace.Event{{Name: "message"}},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}},
}, {
description: "fatal",
spanName: "test",
logLevel: lFatal,
fields: log.Fields{"testKey1": "value1"},
msg: "message",
expectedEvents: []sdktrace.Event{{Name: "message"}},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}},
},
}
for _, tt := range testCases {
t.Run(tt.description, func(t *testing.T) {
tearDown, p, e := setupSuite()
defer tearDown(p)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, s := otel.Tracer(tt.spanName).Start(ctx, tt.spanName)
fl := &fakeLogger{}
l := logger{s: s, l: fl, a: make([]attribute.KeyValue, 0)}
switch tt.logLevel {
case lDebug:
l.WithFields(tt.fields).Debug(tt.msg)
case lInfo:
l.WithFields(tt.fields).Info(tt.msg)
case lWarn:
l.WithFields(tt.fields).Warn(tt.msg)
case lErr:
l.WithFields(tt.fields).Error(tt.msg)
case lFatal:
l.WithFields(tt.fields).Fatal(tt.msg)
}
s.End()
assert.Assert(t, len(e.events) == len(tt.expectedEvents))
for i, event := range tt.expectedEvents {
assert.Assert(t, e.events[i].Name == event.Name)
assert.Assert(t, !e.events[i].Time.IsZero())
}
assert.Assert(t, len(fl.a) == len(tt.expectedAttributes))
for _, a := range tt.expectedAttributes {
cmp.Contains(fl.a, a)
}
})
}
}
func TestLogf(t *testing.T) {
testCases := []struct {
description string
spanName string
logLevel logLevel
msg string
fields log.Fields
args []interface{}
expectedEvents []sdktrace.Event
expectedAttributes []attribute.KeyValue
}{
{
description: "debug",
spanName: "test",
logLevel: lDebug,
msg: "k1: %s, k2: %v, k3: %d, k4: %v",
fields: map[string]interface{}{"k1": "test", "k2": []string{"test"}, "k3": 1, "k4": []int{1}},
args: []interface{}{"test", []string{"test"}, int(1), []int{1}},
expectedEvents: []sdktrace.Event{{Name: "k1: test, k2: [test], k3: 1, k4: [1]"}},
expectedAttributes: []attribute.KeyValue{
attribute.String("k1", "test"),
attribute.String("k2", fmt.Sprintf("%+v", []string{"test"})),
attribute.Int("k3", 1),
attribute.String("k4", fmt.Sprintf("%+v", []int{1})),
},
}, {
description: "info",
spanName: "test",
logLevel: lInfo,
msg: "k1: %d, k2: %v, k3: %f, k4: %v",
fields: map[string]interface{}{"k1": int64(3), "k2": []int64{4}, "k3": float64(2), "k4": []float64{4}},
args: []interface{}{int64(3), []int64{4}, float64(2), []float64{4}},
expectedEvents: []sdktrace.Event{{Name: "k1: 3, k2: [4], k3: 2.000000, k4: [4]"}},
expectedAttributes: []attribute.KeyValue{
attribute.Int64("k1", 1),
attribute.String("k2", fmt.Sprintf("%+v", []int64{2})),
attribute.Float64("k3", 3),
attribute.String("k4", fmt.Sprintf("%+v", []float64{4})),
},
}, {
description: "warn",
spanName: "test",
logLevel: lWarn,
msg: "k1: %v, k2: %v",
fields: map[string]interface{}{"k1": map[int]int{1: 1}, "k2": num(1)},
args: []interface{}{map[int]int{1: 1}, num(1)},
expectedEvents: []sdktrace.Event{{Name: "k1: map[1:1], k2: 1"}},
expectedAttributes: []attribute.KeyValue{
attribute.String("k1", "{1:1}"),
attribute.Stringer("k2", num(1)),
},
}, {
description: "error",
spanName: "test",
logLevel: lErr,
msg: "k1: %t, k2: %v, k3: %s",
fields: map[string]interface{}{"k1": true, "k2": []bool{true}, "k3": errors.New("fake")},
args: []interface{}{true, []bool{true}, errors.New("fake")},
expectedEvents: []sdktrace.Event{{Name: "k1: true, k2: [true], k3: fake"}},
expectedAttributes: []attribute.KeyValue{
attribute.Bool("k1", true),
attribute.String("k2", fmt.Sprintf("%+v", []bool{true})),
attribute.String("k3", "fake"),
},
}, {
description: "fatal",
spanName: "test",
logLevel: lFatal,
expectedEvents: []sdktrace.Event{{Name: ""}},
expectedAttributes: []attribute.KeyValue{},
},
}
for _, tt := range testCases {
t.Run(tt.description, func(t *testing.T) {
tearDown, p, e := setupSuite()
defer tearDown(p)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, s := otel.Tracer(tt.spanName).Start(ctx, tt.spanName)
fl := &fakeLogger{}
l := logger{s: s, l: fl, a: make([]attribute.KeyValue, 0)}
switch tt.logLevel {
case lDebug:
l.WithFields(tt.fields).Debugf(tt.msg, tt.args...)
case lInfo:
l.WithFields(tt.fields).Infof(tt.msg, tt.args...)
case lWarn:
l.WithFields(tt.fields).Warnf(tt.msg, tt.args...)
case lErr:
l.WithFields(tt.fields).Errorf(tt.msg, tt.args...)
case lFatal:
l.WithFields(tt.fields).Fatalf(tt.msg, tt.args...)
}
s.End()
assert.Assert(t, len(e.events) == len(tt.expectedEvents))
for i, event := range tt.expectedEvents {
t.Run(fmt.Sprintf("event %s", event.Name), func(t *testing.T) {
assert.Check(t, cmp.Equal(e.events[i].Name, event.Name))
assert.Check(t, !e.events[i].Time.IsZero())
})
}
assert.Assert(t, cmp.Len(fl.a, len(tt.expectedAttributes)))
sort.Slice(tt.expectedAttributes, func(i, j int) bool {
return tt.expectedAttributes[i].Key < tt.expectedAttributes[j].Key
})
sort.Slice(fl.a, func(i, j int) bool {
return fl.a[i].Key < fl.a[j].Key
})
for i, a := range tt.expectedAttributes {
t.Run(fmt.Sprintf("attribute %s", a.Key), func(t *testing.T) {
assert.Assert(t, fl.a[i].Key == a.Key)
assert.Assert(t, cmp.Equal(fl.a[i].Value.Type(), a.Value.Type()))
// TODO: check value, this is harder to do since the types are unknown
})
}
l.Debugf(tt.msg, tt.args) // should not panic even if span is finished
})
}
}
func TestLogWithField(t *testing.T) {
type field struct {
key string
value interface{}
}
testCases := []struct {
description string
spanName string
fields []field
expectedAttributes []attribute.KeyValue
}{
{
description: "single field",
spanName: "test",
fields: []field{{key: "testKey1", value: "value1"}},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}},
}, {
description: "multiple unique fields",
spanName: "test",
fields: []field{{key: "testKey1", value: "value1"}, {key: "testKey2", value: "value2"}},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}, {Key: "testKey2", Value: attribute.StringValue("value2")}},
}, {
description: "duplicated fields",
spanName: "test",
fields: []field{{key: "testKey1", value: "value1"}, {key: "testKey1", value: "value2"}},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}, {Key: "testKey2", Value: attribute.StringValue("value2")}},
},
}
for _, tt := range testCases {
t.Run(tt.description, func(t *testing.T) {
tearDown, p, _ := setupSuite()
defer tearDown(p)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, s := otel.Tracer(tt.spanName).Start(ctx, tt.spanName)
fl := &fakeLogger{}
l := logger{s: s, l: fl, a: make([]attribute.KeyValue, 0)}
for _, f := range tt.fields {
l.WithField(f.key, f.value).Info("")
}
s.End()
assert.Assert(t, len(fl.a) == len(tt.expectedAttributes))
for _, a := range tt.expectedAttributes {
cmp.Contains(fl.a, a)
}
l.Debug("") // should not panic even if span is finished
})
}
}
func TestLogWithError(t *testing.T) {
testCases := []struct {
description string
spanName string
err error
expectedAttributes []attribute.KeyValue
}{
{
description: "normal",
spanName: "test",
err: errors.New("fake"),
expectedAttributes: []attribute.KeyValue{{Key: "err", Value: attribute.StringValue("fake")}},
}, {
description: "nil error",
spanName: "test",
err: nil,
expectedAttributes: []attribute.KeyValue{{Key: "err", Value: attribute.StringValue("")}},
},
}
for _, tt := range testCases {
t.Run(tt.description, func(t *testing.T) {
tearDown, p, _ := setupSuite()
defer tearDown(p)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, s := otel.Tracer(tt.spanName).Start(ctx, tt.spanName)
fl := &fakeLogger{}
l := logger{s: s, l: fl, a: make([]attribute.KeyValue, 0)}
l.WithError(tt.err).Error("")
s.End()
assert.Assert(t, len(fl.a) == len(tt.expectedAttributes))
for _, a := range tt.expectedAttributes {
cmp.Contains(fl.a, a)
}
})
}
}
func TestLogWithFields(t *testing.T) {
testCases := []struct {
description string
spanName string
fields log.Fields
expectedAttributes []attribute.KeyValue
}{
{
description: "single field",
spanName: "test",
fields: log.Fields{"testKey1": "value1"},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}},
}, {
description: "multiple unique fields",
spanName: "test",
fields: log.Fields{"testKey1": "value1", "testKey2": "value2"},
expectedAttributes: []attribute.KeyValue{{Key: "testKey1", Value: attribute.StringValue("value1")}, {Key: "testKey2", Value: attribute.StringValue("value2")}},
},
}
for _, tt := range testCases {
t.Run(tt.description, func(t *testing.T) {
tearDown, p, _ := setupSuite()
defer tearDown(p)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, s := otel.Tracer(tt.spanName).Start(ctx, tt.spanName)
fl := &fakeLogger{}
l := logger{s: s, l: fl, a: make([]attribute.KeyValue, 0)}
l.WithFields(tt.fields).Debug("")
s.End()
assert.Assert(t, len(fl.a) == len(tt.expectedAttributes))
for _, a := range tt.expectedAttributes {
cmp.Contains(fl.a, a)
}
})
}
}
func setupSuite() (func(provider *sdktrace.TracerProvider), *sdktrace.TracerProvider, *fakeExporter) {
r := NewResource("virtual-kubelet", "1.2.3")
e := &fakeExporter{}
p := sdktrace.NewTracerProvider(
sdktrace.WithSyncer(e),
sdktrace.WithResource(r),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
otel.SetTracerProvider(p)
// Return a function to teardown the test
return func(provider *sdktrace.TracerProvider) {
_ = p.Shutdown(context.Background())
}, p, e
}
func NewResource(name, version string) *resource.Resource {
return resource.NewWithAttributes(name,
semconv.ServiceVersionKey.String(version),
)
}
type fakeExporter struct {
sync.Mutex
// attributes describe the aspects of the spans.
attributes []attribute.KeyValue
// Links returns all the links the span has to other spans.
links []sdktrace.Link
// Events returns all the events that occurred within in the spans
// lifetime.
events []sdktrace.Event
// Status returns the spans status.
status codes.Code
}
func (f *fakeExporter) ExportSpans(_ context.Context, spans []sdktrace.ReadOnlySpan) error {
f.Lock()
defer f.Unlock()
f.attributes = make([]attribute.KeyValue, 0)
f.links = make([]sdktrace.Link, 0)
f.events = make([]sdktrace.Event, 0)
for _, s := range spans {
f.attributes = append(f.attributes, s.Attributes()...)
f.links = append(f.links, s.Links()...)
f.events = append(f.events, s.Events()...)
f.status = s.Status().Code
}
return nil
}
func (f *fakeExporter) Shutdown(_ context.Context) (err error) {
f.attributes = make([]attribute.KeyValue, 0)
f.links = make([]sdktrace.Link, 0)
f.events = make([]sdktrace.Event, 0)
return
}
type fakeLogger struct {
a []attribute.KeyValue
}
func (*fakeLogger) Debug(...interface{}) {}
func (*fakeLogger) Debugf(string, ...interface{}) {}
func (*fakeLogger) Info(...interface{}) {}
func (*fakeLogger) Infof(string, ...interface{}) {}
func (*fakeLogger) Warn(...interface{}) {}
func (*fakeLogger) Warnf(string, ...interface{}) {}
func (*fakeLogger) Error(...interface{}) {}
func (*fakeLogger) Errorf(string, ...interface{}) {}
func (*fakeLogger) Fatal(...interface{}) {}
func (*fakeLogger) Fatalf(string, ...interface{}) {}
func (l *fakeLogger) WithField(k string, v interface{}) log.Logger {
l.a = append(l.a, makeAttribute(k, v))
return l
}
func (l *fakeLogger) WithFields(fs log.Fields) log.Logger {
for k, v := range fs {
l.a = append(l.a, makeAttribute(k, v))
}
return l
}
func (l *fakeLogger) WithError(err error) log.Logger {
l.a = append(l.a, makeAttribute("err", err))
return l
}
type num int
func (i num) String() string {
return strconv.Itoa(int(i))
}

View File

@@ -38,7 +38,7 @@ flags:
default: virtual-kubelet
- name: --os
arg: string
description: The operating system (must be `Linux` or `Windows`)
description: The operating system (must be `linux` or `windows`)
default: Linux
- name: --pod-sync-workers
arg: int

View File

@@ -19,7 +19,16 @@
tag: huawei-cci
- name: HashiCorp Nomad
tag: nomad
- name: InterLink
tag: interlink
org: intertwin-eu
- name: Liqo
tag: liqo
org: liqotech
- name: OpenStack Zun
tag: openstack-zun
- name: Tencent Games Tensile Kube
tag: tensile-kube
- name: StackPath Edge Compute
tag: virtual-kubelet-stackpath
org: stackpath