Compare commits
197 Commits
v1.5.0
...
feat/rest-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfe657cfa5 | ||
|
|
c01fff766d | ||
|
|
e72a73af61 | ||
|
|
84502b708d | ||
|
|
adde4a85df | ||
|
|
4ce429e6dd | ||
|
|
c9f64e8728 | ||
|
|
65f6275bdf | ||
|
|
0a453f92cf | ||
|
|
dd685be418 | ||
|
|
ca78265381 | ||
|
|
5c534ffcd6 | ||
|
|
47e15ff7ee | ||
|
|
d032e56103 | ||
|
|
aff0d1483c | ||
|
|
b8cfb1bf38 | ||
|
|
2ea2063625 | ||
|
|
038f20adc0 | ||
|
|
992c65f9be | ||
|
|
0d14dffb16 | ||
|
|
d29a09c8ba | ||
|
|
d2837b1657 | ||
|
|
8ff762fb7b | ||
|
|
164bd5b3e4 | ||
|
|
b4c0110e35 | ||
|
|
fafac75ae7 | ||
|
|
78b11dfcbb | ||
|
|
f3c3f0af9a | ||
|
|
f37cdd9215 | ||
|
|
3611df8e83 | ||
|
|
4942ea59a7 | ||
|
|
1f5e83b1d6 | ||
|
|
921890c7cb | ||
|
|
0ab48b08ae | ||
|
|
7676cc320c | ||
|
|
8f2e4f2064 | ||
|
|
cd93c79e07 | ||
|
|
5b9c1ce860 | ||
|
|
6aeccbef59 | ||
|
|
76547669a8 | ||
|
|
762d5fb2a5 | ||
|
|
6ae69e26f5 | ||
|
|
663557ce2b | ||
|
|
47c22a831d | ||
|
|
b6e6295c17 | ||
|
|
a3d681ee27 | ||
|
|
154fe08556 | ||
|
|
238aebad73 | ||
|
|
2ee2d9f63b | ||
|
|
d41d02609f | ||
|
|
1236adf762 | ||
|
|
617c3f0615 | ||
|
|
a9ce60e19d | ||
|
|
d6a4e8ee09 | ||
|
|
1197d5907b | ||
|
|
11c9c6cda0 | ||
|
|
0b9dcbb100 | ||
|
|
21a7a61cd2 | ||
|
|
ce8a0ee8bd | ||
|
|
440bcf0535 | ||
|
|
dd31d67a53 | ||
|
|
c5478eabb2 | ||
|
|
394128a0f2 | ||
|
|
72045b221b | ||
|
|
34c2ecc2ef | ||
|
|
da4b353793 | ||
|
|
c590daf8f0 | ||
|
|
7699abc706 | ||
|
|
acd162bf4d | ||
|
|
ff0d16c4c7 | ||
|
|
84af992d29 | ||
|
|
0f2ca47f3d | ||
|
|
816fde5dcb | ||
|
|
c461c8d9d4 | ||
|
|
59fd7fddb6 | ||
|
|
8205ee2889 | ||
|
|
f21325ab41 | ||
|
|
077ee93fa2 | ||
|
|
ad4739b7e4 | ||
|
|
9a9e2ed47a | ||
|
|
52eacaa577 | ||
|
|
4a14603c56 | ||
|
|
2c155accb7 | ||
|
|
9c32bfb0ae | ||
|
|
b7030b9dc5 | ||
|
|
a457d445a3 | ||
|
|
b70ee9b6dd | ||
|
|
8bf7691f59 | ||
|
|
d87cc6ee1a | ||
|
|
2b6bd337cc | ||
|
|
a2070739bb | ||
|
|
a90f71b9a4 | ||
|
|
dcbb102f53 | ||
|
|
90f81e9cc7 | ||
|
|
eb5d959215 | ||
|
|
109b1eed8b | ||
|
|
5e4340a4a4 | ||
|
|
b8f8449177 | ||
|
|
d23c36eec6 | ||
|
|
7bcacb1cab | ||
|
|
a610358f56 | ||
|
|
704b01eac6 | ||
|
|
94999fc0b6 | ||
|
|
9d8005e4b8 | ||
|
|
a486eaffd2 | ||
|
|
d66366ba96 | ||
|
|
bb4e20435d | ||
|
|
6feafcf018 | ||
|
|
5db1443e33 | ||
|
|
2c4442b17f | ||
|
|
70848cfdae | ||
|
|
c668ae6ab6 | ||
|
|
f7f8b45117 | ||
|
|
5001135763 | ||
|
|
67be3c681d | ||
|
|
fca742986c | ||
|
|
db7f53c1ca | ||
|
|
c63b8f0dec | ||
|
|
83fbc0c687 | ||
|
|
d2523fe808 | ||
|
|
7ee822ec6d | ||
|
|
0b70fb1958 | ||
|
|
a25c1def45 | ||
|
|
d682bb3894 | ||
|
|
6198b02423 | ||
|
|
9d94eea9e9 | ||
|
|
de4fe42586 | ||
|
|
305e33bfbf | ||
|
|
00d8340a64 | ||
|
|
5e5a842dbb | ||
|
|
aa94284712 | ||
|
|
d87dd1c79f | ||
|
|
ab3615b8d7 | ||
|
|
22d2416dc4 | ||
|
|
e1c6e80a7a | ||
|
|
1ed3180ec2 | ||
|
|
97452b493f | ||
|
|
6363360781 | ||
|
|
44d0df547d | ||
|
|
10a7559b83 | ||
|
|
b98ba29b52 | ||
|
|
008fe17b91 | ||
|
|
ec1fe2070a | ||
|
|
48e29d75fc | ||
|
|
f617ccebc5 | ||
|
|
433e0bbd20 | ||
|
|
a8f253088c | ||
|
|
38e662129d | ||
|
|
95bdbdec0d | ||
|
|
1958686b4a | ||
|
|
36397f80c2 | ||
|
|
801b44543c | ||
|
|
853f9ead1c | ||
|
|
915445205f | ||
|
|
2b7e4c9dc6 | ||
|
|
410e05878a | ||
|
|
70c7745444 | ||
|
|
269ef14a7a | ||
|
|
faaf14c68d | ||
|
|
7c9bd20eea | ||
|
|
c9c0d99064 | ||
|
|
4974e062d0 | ||
|
|
e1342777d6 | ||
|
|
597e7dc281 | ||
|
|
a9a0ee50cf | ||
|
|
5fe8a7d000 | ||
|
|
22f329fcf0 | ||
|
|
09ad3fe644 | ||
|
|
68347d4ed1 | ||
|
|
92f8661031 | ||
|
|
f63c23108f | ||
|
|
db5bf2b0d3 | ||
|
|
fbf6a1957f | ||
|
|
e6fc00e8dd | ||
|
|
50f1346977 | ||
|
|
66fc9d476f | ||
|
|
0543245668 | ||
|
|
d245d9b8cf | ||
|
|
4fe8496dd1 | ||
|
|
5cd25230c5 | ||
|
|
04cdec767b | ||
|
|
822dc8bb4a | ||
|
|
40b4425804 | ||
|
|
be0a062aec | ||
|
|
a2515d859a | ||
|
|
0df7ac4e80 | ||
|
|
96eae1906b | ||
|
|
8437e237be | ||
|
|
baa0e6e8fc | ||
|
|
405d5d63b1 | ||
|
|
e1486ade00 | ||
|
|
4c223a8cd9 | ||
|
|
bf3a764409 | ||
|
|
b259cb0548 | ||
|
|
e95023b76e | ||
|
|
5fd08d4619 | ||
|
|
c40a255eae |
@@ -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
|
||||
@@ -1,4 +1,6 @@
|
||||
.vscode
|
||||
private.env
|
||||
*.private.*
|
||||
providers/azurebatch/deployment/
|
||||
providers/azurebatch/deployment/
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal 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
115
.github/workflows/ci.yml
vendored
Normal 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
|
||||
65
.github/workflows/codeql-analysis.yml
vendored
65
.github/workflows/codeql-analysis.yml
vendored
@@ -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
5
.gitignore
vendored
@@ -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
34
.golangci.bck.yml
Normal 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
|
||||
@@ -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
|
||||
|
||||
20
Dockerfile
20
Dockerfile
@@ -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
|
||||
|
||||
53
Makefile
53
Makefile
@@ -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 .
|
||||
|
||||
14
Makefile.e2e
14
Makefile.e2e
@@ -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)
|
||||
|
||||
32
README.md
32
README.md
@@ -1,5 +1,7 @@
|
||||
# Virtual Kubelet
|
||||
|
||||
[](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).
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
296
docs/proposals/MetricsUpdateProposal.md
Normal file
296
docs/proposals/MetricsUpdateProposal.md
Normal 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
33
errdefs/auth.go
Normal 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
183
go.mod
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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{
|
||||
|
||||
24
internal/kubernetes/portforward/constants.go
Normal file
24
internal/kubernetes/portforward/constants.go
Normal 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}
|
||||
317
internal/kubernetes/portforward/httpstream.go
Normal file
317
internal/kubernetes/portforward/httpstream.go
Normal 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)
|
||||
}
|
||||
}
|
||||
267
internal/kubernetes/portforward/httpstream_test.go
Normal file
267
internal/kubernetes/portforward/httpstream_test.go
Normal 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
|
||||
}
|
||||
54
internal/kubernetes/portforward/portforward.go
Normal file
54
internal/kubernetes/portforward/portforward.go
Normal 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
|
||||
}
|
||||
}
|
||||
199
internal/kubernetes/portforward/websocket.go
Normal file
199
internal/kubernetes/portforward/websocket.go
Normal 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())
|
||||
}
|
||||
}
|
||||
101
internal/kubernetes/portforward/websocket_test.go
Normal file
101
internal/kubernetes/portforward/websocket_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
79
internal/kubernetes/remotecommand/attach.go
Normal file
79
internal/kubernetes/remotecommand/attach.go
Normal 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,
|
||||
}})
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
15
internal/podutils/README.md
Normal file
15
internal/podutils/README.md
Normal 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 |
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
112
internal/podutils/envvars.go
Normal file
112
internal/podutils/envvars.go
Normal 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
156
internal/podutils/helper.go
Normal 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 != ""
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(×, 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(×, 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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
109
log/slog/slog.go
Normal 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
148
node/api/attach.go
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
67
node/api/metrics.go
Normal 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
149
node/api/metrics_test.go
Normal 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: ×tamp}
|
||||
}
|
||||
@@ -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
116
node/api/portforward.go
Normal 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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
69
node/node.go
69
node/node.go
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
234
node/nodeutil/auth.go
Normal 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"
|
||||
}
|
||||
@@ -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
445
node/nodeutil/controller.go
Normal 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
150
node/nodeutil/opts.go
Normal 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
84
node/nodeutil/provider.go
Normal 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
131
node/nodeutil/tls.go
Normal 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
|
||||
}
|
||||
}
|
||||
32
node/pod.go
32
node/pod.go
@@ -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
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
34
node/queue.go
Normal 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
|
||||
11
node/sync.go
11
node/sync.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
266
trace/opentelemetry/opentelemetry.go
Normal file
266
trace/opentelemetry/opentelemetry.go
Normal 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))
|
||||
}
|
||||
}
|
||||
651
trace/opentelemetry/opentelemetry_test.go
Normal file
651
trace/opentelemetry/opentelemetry_test.go
Normal 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))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user