Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
616538ef01 | ||
|
|
c4582ccfbc | ||
|
|
7feb175720 | ||
|
|
0e1cc1566e | ||
|
|
d11968a0fd | ||
|
|
3ff1694252 | ||
|
|
53e96e03a9 | ||
|
|
3a361ebabd | ||
|
|
ac9a1af564 | ||
|
|
fd3da8dcad | ||
|
|
731d0d6f5c | ||
|
|
2ac4ff9b35 | ||
|
|
82452a73a5 | ||
|
|
2fa03a15a2 | ||
|
|
eb7553e6c4 |
@@ -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"
|
||||
110
.github/workflows/ci.yml
vendored
Normal file
110
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
name: CI
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.18"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.48.0
|
||||
args: --timeout=5m
|
||||
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run Tests
|
||||
run: make test
|
||||
|
||||
env-tests:
|
||||
name: Envtest Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run Tests
|
||||
run: make envtest
|
||||
|
||||
e2e:
|
||||
name: E2E
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CHANGE_MINIKUBE_NONE_USER: true
|
||||
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:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Skaffold
|
||||
run: |
|
||||
curl -Lo 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
|
||||
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
|
||||
- name: Install Minikube
|
||||
run: |
|
||||
curl -Lo 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 -E PATH=$PATH minikube start --vm-driver=none --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@v3
|
||||
|
||||
# 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@v2
|
||||
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@v2
|
||||
|
||||
# ℹ️ 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@v2
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -41,3 +41,5 @@ loganalytics.json
|
||||
**/terraform-provider-kubernetes
|
||||
**/*.tfstate*
|
||||
debug
|
||||
|
||||
vendor/
|
||||
|
||||
@@ -2,28 +2,38 @@ linter-settings:
|
||||
lll:
|
||||
line-length: 200
|
||||
|
||||
timeout: 10m
|
||||
|
||||
run:
|
||||
skip-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
|
||||
- govet
|
||||
- ineffassign
|
||||
- golint
|
||||
- goconst
|
||||
- goimports
|
||||
- unused
|
||||
- structcheck
|
||||
- varcheck
|
||||
- deadcode
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- gofmt
|
||||
- goimports
|
||||
- ineffassign
|
||||
- vet
|
||||
- unused
|
||||
- misspell
|
||||
- nolintlint
|
||||
- gocritic
|
||||
- gosec
|
||||
- exportloopref # Checks for pointers to enclosing loop variables
|
||||
- tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17
|
||||
|
||||
linters-settings:
|
||||
gosec:
|
||||
excludes:
|
||||
- G304
|
||||
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
|
||||
- Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). (is not checked|Errors unhandled)
|
||||
|
||||
20
Dockerfile
20
Dockerfile
@@ -1,4 +1,6 @@
|
||||
FROM golang:1.15 as builder
|
||||
ARG GOLANG_CI_LINT_VERSION
|
||||
|
||||
FROM golang:1.18 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
|
||||
|
||||
38
Makefile
38
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)
|
||||
@@ -187,12 +177,16 @@ kubebuilder_2.3.1_${TEST_OS}_${TEST_ARCH}: kubebuilder_2.3.1_${TEST_OS}_${TEST_A
|
||||
envtest: kubebuilder_2.3.1_${TEST_OS}_${TEST_ARCH}
|
||||
# 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
|
||||
KUBEBUILDER_ASSETS=$(PWD)/kubebuilder_2.3.1_${TEST_OS}_${TEST_ARCH}/bin $(GOTEST) -run=TestEnvtest ./node -envtest=true
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
goimports -w $(shell go list -f '{{.Dir}}' ./...)
|
||||
|
||||
|
||||
export GOLANG_CI_LINT_VERSION ?= v1.48.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 .
|
||||
|
||||
@@ -39,7 +39,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)
|
||||
|
||||
21
README.md
21
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,7 @@ 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)
|
||||
+ [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 +49,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 +137,12 @@ would on a Kubernetes node.
|
||||
|
||||
For detailed instructions, follow the guide [here](https://github.com/virtual-kubelet/nomad/blob/master/README.md).
|
||||
|
||||
### 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
|
||||
@@ -264,6 +273,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 +307,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](virtualkubelet-dev@lists.cncf.io).
|
||||
|
||||
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).
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# The Virtual Kubelet Helm chart
|
||||
|
||||
Each version of Virtual Kubelet has a dedicated [Helm](https://helm.sh) chart. Those charts are served as static assets directly from GitHub.
|
||||
|
||||
## The `index.yaml` file
|
||||
|
||||
This subdirectory has an `index.yaml` file, which is necessary for it to act as a Helm chart repository. To re-generate the `index.yaml` file (assuming that you have Helm installed):
|
||||
|
||||
```shell
|
||||
cd /path/to/virtual-kubelet
|
||||
helm repo index charts
|
||||
```
|
||||
|
||||
The `index.yaml` then needs to be committed to Git and merged to `master`.
|
||||
@@ -1,231 +0,0 @@
|
||||
apiVersion: v1
|
||||
entries:
|
||||
virtual-kubelet:
|
||||
- appVersion: "0.5"
|
||||
created: 2019-01-28T11:18:23.622097-08:00
|
||||
description: A Helm chart to install virtual kubelet inside a Kubernetes cluster.
|
||||
digest: b4af3cc07e1a914b862ebcd05facbf155b16e098a138fcd7f5dc8ac715aabfa2
|
||||
icon: https://avatars2.githubusercontent.com/u/34250142
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-0.5.0.tgz
|
||||
version: 0.5.0
|
||||
- appVersion: "0.5"
|
||||
created: 2019-01-28T11:18:23.62762-08:00
|
||||
description: A Helm chart to install virtual kubelet inside a Kubernetes cluster.
|
||||
digest: b4af3cc07e1a914b862ebcd05facbf155b16e098a138fcd7f5dc8ac715aabfa2
|
||||
icon: https://avatars2.githubusercontent.com/u/34250142
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-latest.tgz
|
||||
version: 0.5.0
|
||||
- appVersion: "0.4"
|
||||
created: 2019-01-28T11:18:23.62156-08:00
|
||||
description: A Helm chart to install virtual kubelet inside a Kubernetes cluster.
|
||||
digest: ae4b8be9d69129f1002ea2228848ec790ea1280d9ff0f7dd99d0d6e3e13922f2
|
||||
icon: https://avatars2.githubusercontent.com/u/34250142
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-0.4.0.tgz
|
||||
version: 0.4.0
|
||||
- appVersion: "0.3"
|
||||
created: 2019-01-28T11:18:23.620905-08:00
|
||||
description: A Helm chart to install virtual kubelet inside a Kubernetes cluster.
|
||||
digest: f472f181c0724420d4f12218f8fc7f65d09fa02a8292f418d1537bf71231d643
|
||||
icon: https://avatars2.githubusercontent.com/u/34250142
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-0.3.0.tgz
|
||||
version: 0.3.0
|
||||
- appVersion: "0.3"
|
||||
created: 2019-01-28T11:18:23.6202-08:00
|
||||
description: A Helm chart to install virtual kubelet inside a Kubernetes cluster.
|
||||
digest: 5a2d0a269620ffe49498686161e853f49761ca4f905577fe1b8e552ab85fcaca
|
||||
icon: https://avatars2.githubusercontent.com/u/34250142
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-0.2.0.tgz
|
||||
version: 0.2.0
|
||||
- created: 2019-01-28T11:18:23.619614-08:00
|
||||
description: a Helm chart to install virtual kubelet inside a Kubernetes cluster.
|
||||
digest: be2778949548cfb0cebe638cbe044d89045eb34ffc8d62180b86ff2f0f30b323
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-0.1.3.tgz
|
||||
version: 0.1.3
|
||||
- created: 2019-01-28T11:18:23.618974-08:00
|
||||
description: a Helm chart to install virtual kubelet inside a Kubernetes cluster.
|
||||
digest: b8519c66766d06a68671a2b8940cf44cccdf9321f144dead543f62d16325331e
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-0.1.2.tgz
|
||||
version: 0.1.2
|
||||
- created: 2019-01-28T11:18:23.618406-08:00
|
||||
description: a Helm chart to install virtual kubelet inside a Kubernetes cluster.
|
||||
digest: b15b3d5acde2cc264b5e8624c3bafcc249440c662ae353ef7012493eb0b6abf6
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-0.1.1.tgz
|
||||
version: 0.1.1
|
||||
- created: 2019-01-28T11:18:23.617865-08:00
|
||||
description: a Helm chart to install virtual kubelet inside a Kubernetes cluster.
|
||||
digest: 22c60ad2f7ea71abf58d65e52b91c2b9feca1ad6a15091321f7f12e5c43ecf81
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-0.1.0.tgz
|
||||
version: 0.1.0
|
||||
virtual-kubelet-for-aks:
|
||||
- created: 2019-01-28T11:18:23.622632-08:00
|
||||
description: a Helm chart to install virtual kubelet in an AKS or ACS cluster.
|
||||
digest: 345cda5aeb537129e1ecc3d23c0cf47651454f672694a4402a7bbb5d3f3a91ba
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet-for-aks
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-for-aks-0.1.10.tgz
|
||||
version: 0.1.10
|
||||
- created: 2019-01-28T11:18:23.627017-08:00
|
||||
description: a Helm chart to install virtual kubelet in an AKS or ACS cluster.
|
||||
digest: 345cda5aeb537129e1ecc3d23c0cf47651454f672694a4402a7bbb5d3f3a91ba
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet-for-aks
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-for-aks-latest.tgz
|
||||
version: 0.1.10
|
||||
- created: 2019-01-28T11:18:23.626524-08:00
|
||||
description: a Helm chart to install virtual kubelet in an AKS or ACS cluster.
|
||||
digest: 4a30821657ff4ef4522060c9905e5659656c035126a7a9e650dd9bb54621b9eb
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet-for-aks
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-for-aks-0.1.9.tgz
|
||||
version: 0.1.9
|
||||
- created: 2019-01-28T11:18:23.626081-08:00
|
||||
description: a Helm chart to install virtual kubelet in an AKS or ACS cluster.
|
||||
digest: 18d988bfb4d24b3674c6c690c0445b85cf3969471cfce74f7c49e87483cb497d
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet-for-aks
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-for-aks-0.1.8.tgz
|
||||
version: 0.1.8
|
||||
- created: 2019-01-28T11:18:23.625442-08:00
|
||||
description: a Helm chart to install virtual kubelet in an AKS or ACS cluster.
|
||||
digest: 21c0719fe330981a170810ee4c3e84375106c176de08894827c8208a66f52112
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet-for-aks
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-for-aks-0.1.7.tgz
|
||||
version: 0.1.7
|
||||
- created: 2019-01-28T11:18:23.624524-08:00
|
||||
description: a Helm chart to install virtual kubelet in an AKS or ACS cluster.
|
||||
digest: b5e72d03f04113a46350fa800135a67f5af9b4ffe3d6650bdeebd271b885e5aa
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet-for-aks
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-for-aks-0.1.6.tgz
|
||||
version: 0.1.6
|
||||
- created: 2019-01-28T11:18:23.624071-08:00
|
||||
description: a Helm chart to install virtual kubelet in an AKS or ACS cluster.
|
||||
digest: 5b04abcc63dcc71b5ad25c8368ad1b114accf721e9c475c5a7f962a48fb0ed65
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet-for-aks
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-for-aks-0.1.5.tgz
|
||||
version: 0.1.5
|
||||
- created: 2019-01-28T11:18:23.623547-08:00
|
||||
description: a Helm chart to install virtual kubelet in an AKS or ACS cluster.
|
||||
digest: b639126041dc4f3d0307f6a678d22021ba149f28612eb0bfc3903c75cf1ea414
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet-for-aks
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-for-aks-0.1.4.tgz
|
||||
version: 0.1.4
|
||||
- created: 2019-01-28T11:18:23.623097-08:00
|
||||
description: a Helm chart to install virtual kubelet in an AKS or ACS cluster.
|
||||
digest: 1b428fd99c667482681c83b61753e7327b85ec2644ef4fa0e9366492a0e73cd7
|
||||
maintainers:
|
||||
- email: junjiez@microsoft.com
|
||||
name: Robbie Zhang
|
||||
name: virtual-kubelet-for-aks
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
urls:
|
||||
- virtual-kubelet-for-aks-0.1.3.tgz
|
||||
version: 0.1.3
|
||||
generated: 2019-01-28T11:18:23.615432-08:00
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
name: virtual-kubelet-for-aks
|
||||
version: 0.1.10
|
||||
description: a Helm chart to install virtual kubelet in an AKS or ACS cluster.
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
maintainers:
|
||||
- name: Robbie Zhang
|
||||
email: junjiez@microsoft.com
|
||||
@@ -1,12 +0,0 @@
|
||||
The virtual kubelet is getting deployed on your cluster.
|
||||
|
||||
To verify that virtual kubelet has started, run:
|
||||
|
||||
kubectl --namespace={{ .Release.Namespace }} get pods -l "app={{ template "fullname" . }}"
|
||||
|
||||
{{- if (not .Values.env.apiserverCert) and (not .Values.env.apiserverKey) }}
|
||||
|
||||
Note:
|
||||
TLS key pair not provided for VK HTTP listener. A key pair was generated for you. This generated key pair is not suitable for production use.
|
||||
|
||||
{{- end }}
|
||||
@@ -1,16 +0,0 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 24 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
*/}}
|
||||
{{- define "fullname" -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
@@ -1,14 +0,0 @@
|
||||
{{ if .Values.rbac.install }}
|
||||
apiVersion: "rbac.authorization.k8s.io/{{ .Values.rbac.apiVersion }}"
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ template "fullname" . }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ template "fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: {{ .Values.rbac.roleRef }}
|
||||
{{ end }}
|
||||
@@ -1,79 +0,0 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ template "fullname" . }}
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ template "fullname" . }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ template "fullname" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: KUBELET_PORT
|
||||
value: "10250"
|
||||
- name: ACS_CREDENTIAL_LOCATION
|
||||
value: /etc/acs/azure.json
|
||||
- name: AZURE_TENANT_ID
|
||||
value: {{ .Values.env.azureTenantId }}
|
||||
- name: AZURE_SUBSCRIPTION_ID
|
||||
value: {{ .Values.env.azureSubscriptionId }}
|
||||
- name: AZURE_CLIENT_ID
|
||||
value: {{ .Values.env.azureClientId }}
|
||||
- name: AZURE_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "fullname" . }}
|
||||
key: clientSecret
|
||||
- name: ACI_RESOURCE_GROUP
|
||||
value: {{ .Values.env.aciResourceGroup }}
|
||||
- name: ACI_REGION
|
||||
value: {{ default "westus" .Values.env.aciRegion }}
|
||||
- name: APISERVER_CERT_LOCATION
|
||||
value: /etc/virtual-kubelet/cert.pem
|
||||
- name: APISERVER_KEY_LOCATION
|
||||
value: /etc/virtual-kubelet/key.pem
|
||||
- name: VKUBELET_POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.podIP
|
||||
- name: ACI_EXTRA_USER_AGENT
|
||||
value: {{ printf "helm-chart/aks/%s/%s" .Chart.Name .Chart.Version }}
|
||||
- name: ACI_SUBNET_NAME
|
||||
value: {{ .Values.env.aciVnetSubnetName }}
|
||||
- name: ACI_SUBNET_CIDR
|
||||
value: {{ .Values.env.aciVnetSubnetCIDR }}
|
||||
- name: MASTER_URI
|
||||
value: {{ .Values.env.masterUri }}
|
||||
- name: CLUSTER_CIDR
|
||||
value: {{ .Values.env.clusterCIDR }}
|
||||
- name: KUBE_DNS_IP
|
||||
value: {{ .Values.env.kubeDnsIP }}
|
||||
{{ if .Values.loganalytics.enabled }}
|
||||
- name: LOG_ANALYTICS_AUTH_LOCATION
|
||||
value: /etc/virtual-kubelet/loganalytics.json
|
||||
{{ end }}
|
||||
volumeMounts:
|
||||
- name: credentials
|
||||
mountPath: "/etc/virtual-kubelet"
|
||||
- name: acs-credential
|
||||
mountPath: "/etc/acs/azure.json"
|
||||
command: ["virtual-kubelet"]
|
||||
args: ["--provider", "azure", "--namespace", {{ default "" .Values.env.monitoredNamespace | quote }}, "--nodename", {{ default "virtual-kubelet" .Values.env.nodeName | quote }} , "--os", {{ default "Linux" .Values.env.nodeOsType | quote }} ]
|
||||
volumes:
|
||||
- name: credentials
|
||||
secret:
|
||||
secretName: {{ template "fullname" . }}
|
||||
- name: acs-credential
|
||||
hostPath:
|
||||
path: /etc/kubernetes/azure.json
|
||||
type: File
|
||||
{{ if .Values.rbac.install }}
|
||||
serviceAccountName: {{ template "fullname" . }}
|
||||
{{ end }}
|
||||
nodeSelector:
|
||||
beta.kubernetes.io/os: linux
|
||||
@@ -1,22 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ template "fullname" . }}
|
||||
type: Opaque
|
||||
data:
|
||||
{{- if (not .Values.env.apiserverCert) and (not .Values.env.apiserverKey) }}
|
||||
{{- $ca := genCA "virtual-kubelet-ca" 3650 }}
|
||||
{{- $cn := printf "%s-virtual-kubelet-apiserver" .Release.Name }}
|
||||
{{- $altName1 := printf "%s-virtual-kubelet-apiserver.%s" .Release.Name .Release.Namespace }}
|
||||
{{- $altName2 := printf "%s-virtual-kubelet-apiserver.%s.svc" .Release.Name .Release.Namespace }}
|
||||
{{- $cert := genSignedCert $cn nil (list $altName1 $altName2) 3650 $ca }}
|
||||
cert.pem: {{ b64enc $cert.Cert }}
|
||||
key.pem: {{ b64enc $cert.Key }}
|
||||
{{ else }}
|
||||
cert.pem: {{ quote .Values.env.apiserverCert }}
|
||||
key.pem: {{ quote .Values.env.apiserverKey }}
|
||||
{{ end}}
|
||||
clientSecret: {{ default "" .Values.env.azureClientKey | b64enc | quote }}
|
||||
{{ if .Values.loganalytics.enabled }}
|
||||
loganalytics.json: {{ printf "{\"workspaceID\": \"%s\",\"workspaceKey\": \"%s\"}" (required "workspaceID is required for loganalytics" .Values.loganalytics.workspaceID ) (required "workspaceKey is required for loganalytics" .Values.loganalytics.workspaceKey ) | b64enc | quote }}
|
||||
{{ end }}
|
||||
@@ -1,6 +0,0 @@
|
||||
{{ if .Values.rbac.install }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ template "fullname" . }}
|
||||
{{ end }}
|
||||
@@ -1,34 +0,0 @@
|
||||
image:
|
||||
repository: microsoft/virtual-kubelet
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
env:
|
||||
azureClientId:
|
||||
azureClientKey:
|
||||
azureTenantId:
|
||||
azureSubscriptionId:
|
||||
aciResourceGroup:
|
||||
aciRegion:
|
||||
nodeName:
|
||||
nodeTaint:
|
||||
nodeOsType:
|
||||
apiserverCert:
|
||||
apiserverKey:
|
||||
monitoredNamespace:
|
||||
aciVnetSubnetName:
|
||||
aciVnetSubnetCidr:
|
||||
masterUri:
|
||||
clusterCidr:
|
||||
kubeDnsIp:
|
||||
loganalytics:
|
||||
enabled: false
|
||||
workspaceID:
|
||||
workspaceKey:
|
||||
|
||||
# Install Default RBAC roles and bindings
|
||||
rbac:
|
||||
install: true
|
||||
## RBAC api version
|
||||
apiVersion: v1beta1
|
||||
# Cluster role reference
|
||||
roleRef: cluster-admin
|
||||
Binary file not shown.
@@ -1,10 +0,0 @@
|
||||
name: virtual-kubelet
|
||||
version: 0.5.0
|
||||
appVersion: 0.5
|
||||
description: A Helm chart to install virtual kubelet inside a Kubernetes cluster.
|
||||
icon: https://avatars2.githubusercontent.com/u/34250142
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
maintainers:
|
||||
- name: Robbie Zhang
|
||||
email: junjiez@microsoft.com
|
||||
@@ -1,12 +0,0 @@
|
||||
The virtual kubelet is getting deployed on your cluster.
|
||||
|
||||
To verify that virtual kubelet has started, run:
|
||||
|
||||
kubectl --namespace={{ .Release.Namespace }} get pods -l "app={{ template "vk.name" . }}"
|
||||
|
||||
{{- if (not .Values.apiserverCert) and (not .Values.apiserverKey) }}
|
||||
|
||||
Note:
|
||||
TLS key pair not provided for VK HTTP listener. A key pair was generated for you. This generated key pair is not suitable for production use.
|
||||
|
||||
{{- end }}
|
||||
@@ -1,29 +0,0 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "vk.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 24 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
*/}}
|
||||
{{- define "vk.fullname" -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Standard labels for helm resources
|
||||
*/}}
|
||||
{{- define "vk.labels" -}}
|
||||
labels:
|
||||
heritage: "{{ .Release.Service }}"
|
||||
release: "{{ .Release.Name }}"
|
||||
revision: "{{ .Release.Revision }}"
|
||||
chart: "{{ .Chart.Name }}"
|
||||
chartVersion: "{{ .Chart.Version }}"
|
||||
app: {{ template "vk.name" . }}
|
||||
{{- end -}}
|
||||
@@ -1,15 +0,0 @@
|
||||
{{ if .Values.rbac.install }}
|
||||
apiVersion: "rbac.authorization.k8s.io/{{ .Values.rbac.apiVersion }}"
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ template "vk.fullname" . }}
|
||||
{{ include "vk.labels" . | indent 2 }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ template "vk.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: {{ .Values.rbac.roleRef }}
|
||||
{{ end }}
|
||||
@@ -1,159 +0,0 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ template "vk.fullname" . }}
|
||||
{{ include "vk.labels" . | indent 2 }}
|
||||
component: kubelet
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
{{ include "vk.labels" . | indent 6 }}
|
||||
component: kubelet
|
||||
annotations:
|
||||
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
|
||||
spec:
|
||||
containers:
|
||||
{{- if eq .Values.trace.exporter "jaeger" }}
|
||||
{{- with .Values.traceExporters.jaeger }}
|
||||
{{- if eq .endpoint "" }}
|
||||
- name: {{ tpl .name $ }}
|
||||
image: {{ .image.repository}}:{{.image.tag}}
|
||||
imagePullPolicy: {{ .image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: 16686
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
- name: {{ template "vk.fullname" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: KUBELET_PORT
|
||||
value: "10250"
|
||||
- name: APISERVER_CERT_LOCATION
|
||||
value: /etc/virtual-kubelet/cert.pem
|
||||
- name: APISERVER_KEY_LOCATION
|
||||
value: /etc/virtual-kubelet/key.pem
|
||||
- name: VKUBELET_POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.podIP
|
||||
- name: VKUBELET_TAINT_KEY
|
||||
value: {{ .Values.taint.key }}
|
||||
- name: VKUBELET_TAINT_VALUE
|
||||
value: {{ tpl .Values.taint.value $ }}
|
||||
- name: VKUBELET_TAINT_EFFECT
|
||||
value: {{ .Values.taint.effect }}
|
||||
{{- if eq (required "You must specify a Virtual Kubelet provider" .Values.provider) "azure" }}
|
||||
{{- with .Values.providers.azure }}
|
||||
{{- if .loganalytics.enabled }}
|
||||
- name: LOG_ANALYTICS_AUTH_LOCATION
|
||||
value: /etc/virtual-kubelet/loganalytics.json
|
||||
- name: CLUSTER_RESOURCE_ID
|
||||
value: {{ .loganalytics.clusterResourceId }}
|
||||
{{- end }}
|
||||
{{- if .targetAKS }}
|
||||
- name: ACS_CREDENTIAL_LOCATION
|
||||
value: /etc/acs/azure.json
|
||||
- name: AZURE_TENANT_ID
|
||||
value: {{ .tenantId }}
|
||||
- name: AZURE_SUBSCRIPTION_ID
|
||||
value: {{ .subscriptionId }}
|
||||
- name: AZURE_CLIENT_ID
|
||||
value: {{ .clientId }}
|
||||
- name: AZURE_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "vk.fullname" $ }}
|
||||
key: clientSecret
|
||||
- name: ACI_RESOURCE_GROUP
|
||||
value: {{ .aciResourceGroup }}
|
||||
- name: ACI_REGION
|
||||
value: {{ .aciRegion }}
|
||||
- name: ACI_EXTRA_USER_AGENT
|
||||
value: {{ printf "helm-chart/aks/%s/%s" $.Chart.Name $.Chart.Version }}
|
||||
{{- else }}
|
||||
- name: AZURE_AUTH_LOCATION
|
||||
value: /etc/virtual-kubelet/credentials.json
|
||||
- name: ACI_RESOURCE_GROUP
|
||||
value: {{ required "aciResourceGroup is required" .aciResourceGroup }}
|
||||
- name: ACI_REGION
|
||||
value: {{ required "aciRegion is required" .aciRegion }}
|
||||
- name: ACI_EXTRA_USER_AGENT
|
||||
value: {{ printf "helm-chart/other/%s/%s" $.Chart.Name $.Chart.Version }}
|
||||
{{- end }}
|
||||
{{- if .vnet.enabled }}
|
||||
- name: ACI_SUBNET_NAME
|
||||
value: {{ required "subnetName is required" .vnet.subnetName }}
|
||||
- name: ACI_SUBNET_CIDR
|
||||
value: {{ .vnet.subnetCidr }}
|
||||
- name: MASTER_URI
|
||||
value: {{ required "masterUri is required" .masterUri }}
|
||||
- name: CLUSTER_CIDR
|
||||
value: {{ required "clusterCidr is required" .vnet.clusterCidr }}
|
||||
- name: KUBE_DNS_IP
|
||||
value: {{ required "kubeDnsIp is required" .vnet.kubeDnsIp }}
|
||||
{{- else }}
|
||||
- name: MASTER_URI
|
||||
value: {{ .masterUri }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.trace.exporter "jaeger" }}
|
||||
- name: JAEGER_ENDPOINT
|
||||
{{- with .Values.traceExporters.jaeger }}
|
||||
{{- if eq .endpoint "" }}
|
||||
value: "http://127.0.0.1:14268"
|
||||
{{- else }}
|
||||
value: {{.endpoint}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: credentials
|
||||
mountPath: "/etc/virtual-kubelet"
|
||||
{{- if eq (required "You must specify a Virtual Kubelet provider" .Values.provider) "azure" }}
|
||||
{{- if .Values.providers.azure.targetAKS }}
|
||||
- name: acs-credential
|
||||
mountPath: "/etc/acs/azure.json"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
command: ["virtual-kubelet"]
|
||||
args: [
|
||||
{{- if not .Values.taint.enabled }}
|
||||
"--disable-taint", "true",
|
||||
{{- end }}
|
||||
"--provider", "{{ required "You must specify a Virtual Kubelet provider" .Values.provider }}",
|
||||
"--namespace", "{{ .Values.monitoredNamespace }}",
|
||||
"--nodename", "{{ required "nodeName is required" .Values.nodeName }}",
|
||||
{{- if .Values.logLevel }}
|
||||
"--log-level", "{{.Values.logLevel}}",
|
||||
{{- end }}
|
||||
{{- if ne .Values.trace.exporter "" }}
|
||||
"--trace-exporter", "{{ .Values.trace.exporter }}",
|
||||
{{- if gt .Values.trace.sampleRate 0.0 }}
|
||||
"--trace-sample-rate", "{{ .Values.trace.sampleRate }}",
|
||||
{{- end }}
|
||||
{{- $serviceName := tpl .Values.trace.serviceName $ }}
|
||||
{{- if ne $serviceName "" }}
|
||||
"--trace-service-name", "{{ $serviceName }}",
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
"--os", "{{ .Values.nodeOsType }}"
|
||||
]
|
||||
volumes:
|
||||
- name: credentials
|
||||
secret:
|
||||
secretName: {{ template "vk.fullname" . }}
|
||||
{{- if eq (required "You must specify a Virtual Kubelet provider" .Values.provider) "azure" }}
|
||||
{{- if .Values.providers.azure.targetAKS }}
|
||||
- name: acs-credential
|
||||
hostPath:
|
||||
path: /etc/kubernetes/azure.json
|
||||
type: File
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ if .Values.rbac.install }} "{{ template "vk.fullname" . }}" {{ end }}
|
||||
nodeSelector:
|
||||
beta.kubernetes.io/os: linux
|
||||
@@ -1,31 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ template "vk.fullname" . }}
|
||||
{{ include "vk.labels" . | indent 2 }}
|
||||
type: Opaque
|
||||
data:
|
||||
{{- if (not .Values.apiserverCert) and (not .Values.apiserverKey) }}
|
||||
{{- $ca := genCA "virtual-kubelet-ca" 3650 }}
|
||||
{{- $cn := printf "%s-virtual-kubelet-apiserver" .Release.Name }}
|
||||
{{- $altName1 := printf "%s-virtual-kubelet-apiserver.%s" .Release.Name .Release.Namespace }}
|
||||
{{- $altName2 := printf "%s-virtual-kubelet-apiserver.%s.svc" .Release.Name .Release.Namespace }}
|
||||
{{- $cert := genSignedCert $cn nil (list $altName1 $altName2) 3650 $ca }}
|
||||
cert.pem: {{ b64enc $cert.Cert }}
|
||||
key.pem: {{ b64enc $cert.Key }}
|
||||
{{- else }}
|
||||
cert.pem: {{ quote .Values.apiserverCert }}
|
||||
key.pem: {{ quote .Values.apiserverKey }}
|
||||
{{- end }}
|
||||
{{- if eq (required "You must specify a Virtual Kubelet provider" .Values.provider) "azure" }}
|
||||
{{- with .Values.providers.azure }}
|
||||
{{- if .loganalytics.enabled }}
|
||||
loganalytics.json: {{ printf "{\"workspaceID\": \"%s\",\"workspaceKey\": \"%s\"}" (required "workspaceId is required for loganalytics" .loganalytics.workspaceId ) (required "workspaceKey is required for loganalytics" .loganalytics.workspaceKey ) | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- if .targetAKS }}
|
||||
clientSecret: {{ default "" .clientKey | b64enc | quote }}
|
||||
{{- else }}
|
||||
credentials.json: {{ printf "{ \"clientId\": \"%s\", \"clientSecret\": \"%s\", \"subscriptionId\": \"%s\", \"tenantId\": \"%s\", \"activeDirectoryEndpointUrl\": \"https://login.microsoftonline.com/\", \"resourceManagerEndpointUrl\": \"https://management.azure.com/\", \"activeDirectoryGraphResourceId\": \"https://graph.windows.net/\", \"sqlManagementEndpointUrl\": \"database.windows.net\", \"galleryEndpointUrl\": \"https://gallery.azure.com/\", \"managementEndpointUrl\": \"https://management.core.windows.net/\" }" (default "MISSING" .clientId) (default "MISSING" .clientKey) (default "MISSING" .subscriptionId) (default "MISSING" .tenantId) | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,7 +0,0 @@
|
||||
{{ if .Values.rbac.install }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ template "vk.fullname" . }}
|
||||
{{ include "vk.labels" . | indent 2 }}
|
||||
{{ end }}
|
||||
@@ -1,30 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ .Release.Name }}-{{ .Release.Revision }}-test"
|
||||
{{ include "vk.labels" . | indent 2 }}
|
||||
component: test
|
||||
annotations:
|
||||
"helm.sh/hook": test-success
|
||||
spec:
|
||||
containers:
|
||||
- image: hello-world:linux
|
||||
imagePullPolicy: Always
|
||||
name: helloworld
|
||||
resources:
|
||||
requests:
|
||||
memory: "0.1G"
|
||||
cpu: 10m
|
||||
limits:
|
||||
memory: "0.1G"
|
||||
cpu: 10m
|
||||
dnsPolicy: ClusterFirst
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: "{{ .Values.nodeName }}"
|
||||
restartPolicy: Never
|
||||
tolerations:
|
||||
{{- if .Values.taint.enabled }}
|
||||
- key: "{{ .Values.taint.key }}"
|
||||
value: "{{ tpl .Values.taint.value $ }}"
|
||||
effect: "{{ .Values.taint.effect }}"
|
||||
{{- end }}
|
||||
@@ -1,65 +0,0 @@
|
||||
image:
|
||||
repository: microsoft/virtual-kubelet
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
nodeName: "virtual-kubelet"
|
||||
nodeOsType: "Linux"
|
||||
monitoredNamespace: ""
|
||||
apiserverCert:
|
||||
apiserverKey:
|
||||
logLevel:
|
||||
|
||||
taint:
|
||||
enabled: true
|
||||
key: virtual-kubelet.io/provider
|
||||
value: "{{ .Values.provider }}"
|
||||
## `effect` must be `NoSchedule`, `PreferNoSchedule` or `NoExecute`.
|
||||
effect: NoSchedule
|
||||
|
||||
trace:
|
||||
exporter: ""
|
||||
serviceName: "{{ .Values.nodeName }}"
|
||||
sampleRate: 0
|
||||
|
||||
traceExporters:
|
||||
jaeger:
|
||||
name: "{{ .Values.trace.exporter }}"
|
||||
endpoint: ""
|
||||
image:
|
||||
repository: jaegertracing/all-in-one
|
||||
tag: 1.8
|
||||
pullPolicy: Always
|
||||
|
||||
providers:
|
||||
azure:
|
||||
## Set to true if deploying to Azure Kubernetes Service (AKS), otherwise false
|
||||
targetAKS: true
|
||||
clientId:
|
||||
clientKey:
|
||||
tenantId:
|
||||
subscriptionId:
|
||||
## `aciResourceGroup` and `aciRegion` are required only for non-AKS deployments
|
||||
aciResourceGroup:
|
||||
aciRegion:
|
||||
masterUri:
|
||||
loganalytics:
|
||||
enabled: false
|
||||
workspaceId:
|
||||
workspaceKey:
|
||||
clusterResourceId:
|
||||
vnet:
|
||||
enabled: false
|
||||
subnetName:
|
||||
subnetCidr:
|
||||
clusterCidr:
|
||||
kubeDnsIp:
|
||||
|
||||
## Install Default RBAC roles and bindings
|
||||
rbac:
|
||||
install: true
|
||||
serviceAccountName: virtual-kubelet
|
||||
## RBAC api version
|
||||
apiVersion: v1beta1
|
||||
## Cluster role reference
|
||||
roleRef: cluster-admin
|
||||
@@ -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,140 +15,15 @@
|
||||
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
|
||||
CACertPath string
|
||||
Addr string
|
||||
MetricsAddr string
|
||||
StreamIdleTimeout time.Duration
|
||||
@@ -157,8 +32,9 @@ type apiServerConfig struct {
|
||||
|
||||
func getAPIConfig(c Opts) (*apiServerConfig, error) {
|
||||
config := apiServerConfig{
|
||||
CertPath: os.Getenv("APISERVER_CERT_LOCATION"),
|
||||
KeyPath: os.Getenv("APISERVER_KEY_LOCATION"),
|
||||
CertPath: os.Getenv("APISERVER_CERT_LOCATION"),
|
||||
KeyPath: os.Getenv("APISERVER_KEY_LOCATION"),
|
||||
CACertPath: os.Getenv("APISERVER_CA_CERT_LOCATION"),
|
||||
}
|
||||
|
||||
config.Addr = fmt.Sprintf(":%d", c.ListenPort)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,8 +16,10 @@ package root
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -26,14 +28,10 @@ import (
|
||||
"github.com/virtual-kubelet/virtual-kubelet/internal/manager"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/log"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/node"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/node/api"
|
||||
"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"
|
||||
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||
)
|
||||
|
||||
// NewCommand creates a new top-level command.
|
||||
@@ -75,30 +73,40 @@ func runRootCommand(ctx context.Context, s *provider.Store, c Opts) error {
|
||||
}
|
||||
}
|
||||
|
||||
client, err := nodeutil.ClientsetFromEnv(c.KubeConfigPath)
|
||||
// Ensure API client.
|
||||
clientSet, err := nodeutil.ClientsetFromEnv(c.KubeConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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),
|
||||
)
|
||||
podInformer := podInformerFactory.Core().V1().Pods()
|
||||
// Set-up the node provider.
|
||||
mux := http.NewServeMux()
|
||||
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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
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
|
||||
}
|
||||
|
||||
apiConfig, err := getAPIConfig(c)
|
||||
@@ -106,28 +114,40 @@ func runRootCommand(ctx context.Context, s *provider.Store, c Opts) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := setupTracing(ctx, c); err != nil {
|
||||
cm, err := nodeutil.NewNode(c.NodeName, newProvider, func(cfg *nodeutil.NodeConfig) error {
|
||||
cfg.KubeconfigPath = c.KubeConfigPath
|
||||
cfg.Handler = mux
|
||||
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.WithClient(clientSet),
|
||||
setAuth(c.NodeName, apiConfig),
|
||||
nodeutil.WithTLSConfig(
|
||||
nodeutil.WithKeyPairFromPath(apiConfig.CertPath, apiConfig.KeyPath),
|
||||
maybeCA(apiConfig.CACertPath),
|
||||
),
|
||||
nodeutil.AttachProviderRoutes(mux),
|
||||
)
|
||||
if err != nil {
|
||||
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)
|
||||
if err := setupTracing(ctx, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(log.Fields{
|
||||
@@ -137,117 +157,54 @@ 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
|
||||
func setAuth(node string, apiCfg *apiServerConfig) nodeutil.NodeOpt {
|
||||
if apiCfg.CACertPath == "" {
|
||||
return func(cfg *nodeutil.NodeConfig) error {
|
||||
cfg.Handler = api.InstrumentHandler(nodeutil.WithAuth(nodeutil.NoAuth(), cfg.Handler))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return func(cfg *nodeutil.NodeConfig) error {
|
||||
auth, err := nodeutil.WebhookAuth(cfg.Client, node, func(cfg *nodeutil.WebhookAuthConfig) error {
|
||||
var err error
|
||||
cfg.AuthnConfig.ClientCertificateCAContentProvider, err = dynamiccertificates.NewDynamicCAContentFromFile("ca-cert-bundle", apiCfg.CACertPath)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.Handler = api.InstrumentHandler(nodeutil.WithAuth(auth, cfg.Handler))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func maybeCA(p string) func(*tls.Config) error {
|
||||
if p == "" {
|
||||
return func(*tls.Config) error { return nil }
|
||||
}
|
||||
return nodeutil.WithCAFromPath(p)
|
||||
}
|
||||
|
||||
@@ -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,19 +5,20 @@ 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"
|
||||
stats "github.com/virtual-kubelet/virtual-kubelet/node/api/statsv1alpha1"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/trace"
|
||||
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"
|
||||
)
|
||||
|
||||
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 { //nolint:golint
|
||||
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 { //nolint:golint
|
||||
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,7 +100,7 @@ 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
|
||||
}
|
||||
@@ -128,6 +131,11 @@ func loadConfig(providerConfig, nodeName string) (config MockConfig, err error)
|
||||
if _, err = resource.ParseQuantity(config.Pods); err != nil {
|
||||
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,13 @@ 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
|
||||
}
|
||||
|
||||
// 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 +343,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) { //nolint:golint
|
||||
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,7 +357,7 @@ 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"
|
||||
@@ -349,11 +367,15 @@ func (p *MockProvider) ConfigureNode(ctx context.Context, n *v1.Node) { // nolin
|
||||
|
||||
// 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 +489,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 +530,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)) {
|
||||
|
||||
@@ -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 { //nolint:golint
|
||||
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) //nolint:golint
|
||||
|
||||
@@ -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 //nolint:golint
|
||||
|
||||
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 { //nolint:golint
|
||||
keys := make([]string, 0, len(o))
|
||||
for k := range o {
|
||||
keys = append(keys, k)
|
||||
|
||||
@@ -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
|
||||
|
||||
172
go.mod
172
go.mod
@@ -1,77 +1,113 @@
|
||||
module github.com/virtual-kubelet/virtual-kubelet
|
||||
|
||||
go 1.15
|
||||
go 1.17
|
||||
|
||||
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.0.0
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/gorilla/mux v1.8.0
|
||||
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_golang v1.14.0
|
||||
github.com/prometheus/client_model v0.3.0
|
||||
github.com/prometheus/common v0.42.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
go.opencensus.io v0.21.0
|
||||
go.uber.org/goleak v1.1.10
|
||||
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.8.0
|
||||
go.opencensus.io v0.23.0
|
||||
go.opentelemetry.io/otel v0.20.0
|
||||
go.opentelemetry.io/otel/sdk v0.20.0
|
||||
go.opentelemetry.io/otel/trace v0.20.0
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
||||
golang.org/x/sys v0.5.0
|
||||
golang.org/x/time v0.3.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
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.26.2
|
||||
k8s.io/apimachinery v0.26.2
|
||||
k8s.io/apiserver v0.25.0
|
||||
k8s.io/client-go v0.25.0
|
||||
k8s.io/klog/v2 v2.90.1
|
||||
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d
|
||||
sigs.k8s.io/controller-runtime v0.13.0
|
||||
)
|
||||
|
||||
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/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.5 // indirect
|
||||
github.com/go-openapi/swag v0.19.14 // 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.2 // indirect
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
github.com/google/gofuzz v1.1.0 // indirect
|
||||
github.com/google/uuid v1.1.2 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/moby/spdystream v0.2.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/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/uber/jaeger-client-go v2.25.0+incompatible // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.4 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.4 // indirect
|
||||
go.opentelemetry.io/contrib v0.20.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.7.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.21.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/oauth2 v0.5.0 // indirect
|
||||
golang.org/x/term v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
google.golang.org/api v0.57.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
|
||||
google.golang.org/grpc v1.47.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.25.0 // indirect
|
||||
k8s.io/component-base v0.25.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.32 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
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,
|
||||
}})
|
||||
}
|
||||
@@ -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,10 +29,6 @@ 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"
|
||||
)
|
||||
|
||||
@@ -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 pointer.String(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 pointer.String(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 pointer.String(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 pointer.String(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 != ""
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
@@ -23,8 +24,10 @@ import (
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/log"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/trace"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/utils/clock"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,58 +35,257 @@ 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
|
||||
|
||||
// Queue implements a wrapper around workqueue with native VK instrumentation
|
||||
type Queue struct {
|
||||
lock sync.Mutex
|
||||
running bool
|
||||
name string
|
||||
workqueue workqueue.RateLimitingInterface
|
||||
handler ItemHandler
|
||||
// clock is used for testing
|
||||
clock clock.Clock
|
||||
// lock protects running, and the items list / map
|
||||
lock sync.Mutex
|
||||
running bool
|
||||
name string
|
||||
handler ItemHandler
|
||||
|
||||
ratelimiter workqueue.RateLimiter
|
||||
// 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
|
||||
itemsInQueue map[string]*list.Element
|
||||
// itemsBeingProcessed is a map of (string) key -> item once it has been moved
|
||||
itemsBeingProcessed map[string]*queueItem
|
||||
// Wait for next semaphore is an exclusive (1 item) lock that is taken every time items is checked to see if there
|
||||
// is an item in queue for work
|
||||
waitForNextItemSemaphore *semaphore.Weighted
|
||||
|
||||
// wakeup
|
||||
wakeupCh chan struct{}
|
||||
|
||||
retryFunc ShouldRetryFunc
|
||||
}
|
||||
|
||||
type queueItem struct {
|
||||
key string
|
||||
plannedToStartWorkAt time.Time
|
||||
redirtiedAt time.Time
|
||||
redirtiedWithRatelimit bool
|
||||
forget bool
|
||||
requeues int
|
||||
|
||||
// Debugging information only
|
||||
originallyAdded time.Time
|
||||
addedViaRedirty bool
|
||||
delayedViaRateLimit *time.Duration
|
||||
}
|
||||
|
||||
func (item *queueItem) String() string {
|
||||
return fmt.Sprintf("<plannedToStartWorkAt:%s key: %s>", item.plannedToStartWorkAt.String(), item.key)
|
||||
}
|
||||
|
||||
// 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.RateLimiter, name string, handler ItemHandler, retryFunc ShouldRetryFunc) *Queue {
|
||||
if retryFunc == nil {
|
||||
retryFunc = DefaultRetryFunc
|
||||
}
|
||||
return &Queue{
|
||||
name: name,
|
||||
workqueue: workqueue.NewNamedRateLimitingQueue(ratelimiter, name),
|
||||
handler: handler,
|
||||
clock: clock.RealClock{},
|
||||
name: name,
|
||||
ratelimiter: ratelimiter,
|
||||
items: list.New(),
|
||||
itemsBeingProcessed: make(map[string]*queueItem),
|
||||
itemsInQueue: make(map[string]*list.Element),
|
||||
handler: handler,
|
||||
wakeupCh: make(chan struct{}, 1),
|
||||
waitForNextItemSemaphore: semaphore.NewWeighted(1),
|
||||
retryFunc: retryFunc,
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue enqueues the key in a rate limited fashion
|
||||
func (q *Queue) Enqueue(key string) {
|
||||
q.workqueue.AddRateLimited(key)
|
||||
func (q *Queue) Enqueue(ctx context.Context, key string) {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
q.insert(ctx, key, true, nil)
|
||||
}
|
||||
|
||||
// EnqueueWithoutRateLimit enqueues the key without a rate limit
|
||||
func (q *Queue) EnqueueWithoutRateLimit(key string) {
|
||||
q.workqueue.Add(key)
|
||||
func (q *Queue) EnqueueWithoutRateLimit(ctx context.Context, key string) {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
q.insert(ctx, key, false, nil)
|
||||
}
|
||||
|
||||
// Forget forgets the key
|
||||
func (q *Queue) Forget(key string) {
|
||||
q.workqueue.Forget(key)
|
||||
func (q *Queue) Forget(ctx context.Context, key string) {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
ctx, span := trace.StartSpan(ctx, "Forget")
|
||||
defer span.End()
|
||||
|
||||
ctx = span.WithFields(ctx, map[string]interface{}{
|
||||
"queue": q.name,
|
||||
"key": key,
|
||||
})
|
||||
|
||||
if item, ok := q.itemsInQueue[key]; ok {
|
||||
span.WithField(ctx, "status", "itemInQueue")
|
||||
delete(q.itemsInQueue, key)
|
||||
q.items.Remove(item)
|
||||
return
|
||||
}
|
||||
|
||||
if qi, ok := q.itemsBeingProcessed[key]; ok {
|
||||
span.WithField(ctx, "status", "itemBeingProcessed")
|
||||
qi.forget = true
|
||||
return
|
||||
}
|
||||
span.WithField(ctx, "status", "notfound")
|
||||
}
|
||||
|
||||
// EnqueueAfter enqueues the item after this period
|
||||
//
|
||||
// Since it wrap workqueue semantics, if an item has been enqueued after, and it is immediately scheduled for work,
|
||||
// it will process the immediate item, and then upon the latter delayed processing it will be processed again
|
||||
func (q *Queue) EnqueueAfter(key string, after time.Duration) {
|
||||
q.workqueue.AddAfter(key, after)
|
||||
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"
|
||||
// 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()
|
||||
|
||||
ctx = span.WithFields(ctx, map[string]interface{}{
|
||||
"queue": q.name,
|
||||
"key": key,
|
||||
"ratelimit": ratelimit,
|
||||
})
|
||||
if delay == nil {
|
||||
ctx = span.WithField(ctx, "delay", "nil")
|
||||
} else {
|
||||
ctx = span.WithField(ctx, "delay", delay.String())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
select {
|
||||
case q.wakeupCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}()
|
||||
|
||||
// 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(durationDeref(delay, 0))
|
||||
// Is the item already been redirtied?
|
||||
if item.redirtiedAt.IsZero() {
|
||||
item.redirtiedAt = when
|
||||
item.redirtiedWithRatelimit = ratelimit
|
||||
} else if when.Before(item.redirtiedAt) {
|
||||
item.redirtiedAt = when
|
||||
item.redirtiedWithRatelimit = ratelimit
|
||||
}
|
||||
item.forget = false
|
||||
return item
|
||||
}
|
||||
|
||||
// Is the item already in the queue?
|
||||
if item, ok := q.itemsInQueue[key]; ok {
|
||||
span.WithField(ctx, "status", "itemsInQueue")
|
||||
qi := item.Value.(*queueItem)
|
||||
when := q.clock.Now().Add(durationDeref(delay, 0))
|
||||
q.adjustPosition(qi, item, when)
|
||||
return qi
|
||||
}
|
||||
|
||||
span.WithField(ctx, "status", "added")
|
||||
now := q.clock.Now()
|
||||
val := &queueItem{
|
||||
key: key,
|
||||
plannedToStartWorkAt: now,
|
||||
originallyAdded: now,
|
||||
}
|
||||
|
||||
if ratelimit {
|
||||
actualDelay := q.ratelimiter.When(key)
|
||||
// Check if delay is overridden
|
||||
if delay != nil {
|
||||
actualDelay = *delay
|
||||
}
|
||||
span.WithField(ctx, "delay", actualDelay.String())
|
||||
val.plannedToStartWorkAt = val.plannedToStartWorkAt.Add(actualDelay)
|
||||
val.delayedViaRateLimit = &actualDelay
|
||||
} else {
|
||||
val.plannedToStartWorkAt = val.plannedToStartWorkAt.Add(durationDeref(delay, 0))
|
||||
}
|
||||
|
||||
for item := q.items.Back(); item != nil; item = item.Prev() {
|
||||
qi := item.Value.(*queueItem)
|
||||
if qi.plannedToStartWorkAt.Before(val.plannedToStartWorkAt) {
|
||||
q.itemsInQueue[key] = q.items.InsertAfter(val, item)
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
q.itemsInQueue[key] = q.items.PushFront(val)
|
||||
return val
|
||||
}
|
||||
|
||||
func (q *Queue) adjustPosition(qi *queueItem, element *list.Element, when time.Time) {
|
||||
if when.After(qi.plannedToStartWorkAt) {
|
||||
// The item has already been delayed appropriately
|
||||
return
|
||||
}
|
||||
|
||||
qi.plannedToStartWorkAt = when
|
||||
for prev := element.Prev(); prev != nil; prev = prev.Prev() {
|
||||
item := prev.Value.(*queueItem)
|
||||
// does this item plan to start work *before* the new time? If so add it
|
||||
if item.plannedToStartWorkAt.Before(when) {
|
||||
q.items.MoveAfter(element, prev)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
q.items.MoveToFront(element)
|
||||
}
|
||||
|
||||
// EnqueueWithoutRateLimitWithDelay enqueues without rate limiting, but work will not start for this given delay period
|
||||
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)
|
||||
}
|
||||
|
||||
// Empty returns if the queue has no items in it
|
||||
//
|
||||
// It should only be used for debugging, as delayed items are not counted, leading to confusion
|
||||
// It should only be used for debugging.
|
||||
func (q *Queue) Empty() bool {
|
||||
return q.workqueue.Len() == 0
|
||||
return q.Len() == 0
|
||||
}
|
||||
|
||||
// Len includes items that are in the queue, and are being processed
|
||||
func (q *Queue) Len() int {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
if q.items.Len() != len(q.itemsInQueue) {
|
||||
panic("Internally inconsistent state")
|
||||
}
|
||||
|
||||
return q.items.Len() + len(q.itemsBeingProcessed)
|
||||
}
|
||||
|
||||
// Run starts the workers
|
||||
@@ -112,13 +314,14 @@ func (q *Queue) Run(ctx context.Context, workers int) {
|
||||
|
||||
group := &wait.Group{}
|
||||
for i := 0; i < workers; i++ {
|
||||
// This is required because i is referencing a mutable variable and that's running in a separate goroutine
|
||||
idx := i
|
||||
group.StartWithContext(ctx, func(ctx context.Context) {
|
||||
q.worker(ctx, i)
|
||||
q.worker(ctx, idx)
|
||||
})
|
||||
}
|
||||
defer group.Wait()
|
||||
<-ctx.Done()
|
||||
q.workqueue.ShutDown()
|
||||
}
|
||||
|
||||
func (q *Queue) worker(ctx context.Context, i int) {
|
||||
@@ -130,6 +333,54 @@ func (q *Queue) worker(ctx context.Context, i int) {
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) getNextItem(ctx context.Context) (*queueItem, error) {
|
||||
if err := q.waitForNextItemSemaphore.Acquire(ctx, 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer q.waitForNextItemSemaphore.Release(1)
|
||||
|
||||
for {
|
||||
q.lock.Lock()
|
||||
element := q.items.Front()
|
||||
if element == nil {
|
||||
// Wait for the next item
|
||||
q.lock.Unlock()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-q.wakeupCh:
|
||||
}
|
||||
} else {
|
||||
qi := element.Value.(*queueItem)
|
||||
timeUntilProcessing := time.Until(qi.plannedToStartWorkAt)
|
||||
|
||||
// Do we need to sleep? If not, let's party.
|
||||
if timeUntilProcessing <= 0 {
|
||||
q.itemsBeingProcessed[qi.key] = qi
|
||||
q.items.Remove(element)
|
||||
delete(q.itemsInQueue, qi.key)
|
||||
q.lock.Unlock()
|
||||
return qi, nil
|
||||
}
|
||||
|
||||
q.lock.Unlock()
|
||||
if err := func() error {
|
||||
timer := q.clock.NewTimer(timeUntilProcessing)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-timer.C():
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-q.wakeupCh:
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleQueueItem handles a single item
|
||||
//
|
||||
// A return value of "false" indicates that further processing should be stopped.
|
||||
@@ -137,8 +388,9 @@ func (q *Queue) handleQueueItem(ctx context.Context) bool {
|
||||
ctx, span := trace.StartSpan(ctx, "handleQueueItem")
|
||||
defer span.End()
|
||||
|
||||
obj, shutdown := q.workqueue.Get()
|
||||
if shutdown {
|
||||
qi, err := q.getNextItem(ctx)
|
||||
if err != nil {
|
||||
span.SetStatus(err)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -146,11 +398,10 @@ func (q *Queue) handleQueueItem(ctx context.Context) bool {
|
||||
// These are of the form namespace/name.
|
||||
// We do this as the delayed nature of the work Queue means the items in the informer cache may actually be more u
|
||||
// to date that when the item was initially put onto the workqueue.
|
||||
key := obj.(string)
|
||||
ctx = span.WithField(ctx, "key", key)
|
||||
ctx = span.WithField(ctx, "key", qi.key)
|
||||
log.G(ctx).Debug("Got Queue object")
|
||||
|
||||
err := q.handleQueueItemObject(ctx, key)
|
||||
err = q.handleQueueItemObject(ctx, qi)
|
||||
if err != nil {
|
||||
// We've actually hit an error, so we set the span's status based on the error.
|
||||
span.SetStatus(err)
|
||||
@@ -162,35 +413,90 @@ func (q *Queue) handleQueueItem(ctx context.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (q *Queue) handleQueueItemObject(ctx context.Context, key string) error {
|
||||
func (q *Queue) handleQueueItemObject(ctx context.Context, qi *queueItem) error {
|
||||
// This is a separate function / span, because the handleQueueItem span is the time spent waiting for the object
|
||||
// plus the time spend handling the object. Instead, this function / span is scoped to a single object.
|
||||
ctx, span := trace.StartSpan(ctx, "handleQueueItemObject")
|
||||
defer span.End()
|
||||
ctx = span.WithField(ctx, "key", key)
|
||||
|
||||
// We call Done here so the work Queue knows we have finished processing this item.
|
||||
// We also must remember to call Forget if we do not want this work item being re-queued.
|
||||
// For example, we do not call Forget if a transient error occurs.
|
||||
// Instead, the item is put back on the work Queue and attempted again after a back-off period.
|
||||
defer q.workqueue.Done(key)
|
||||
ctx = span.WithFields(ctx, map[string]interface{}{
|
||||
"requeues": qi.requeues,
|
||||
"originallyAdded": qi.originallyAdded.String(),
|
||||
"addedViaRedirty": qi.addedViaRedirty,
|
||||
"plannedForWork": qi.plannedToStartWorkAt.String(),
|
||||
})
|
||||
|
||||
if qi.delayedViaRateLimit != nil {
|
||||
ctx = span.WithField(ctx, "delayedViaRateLimit", qi.delayedViaRateLimit.String())
|
||||
}
|
||||
|
||||
// Add the current key as an attribute to the current span.
|
||||
ctx = span.WithField(ctx, "key", key)
|
||||
ctx = span.WithField(ctx, "key", qi.key)
|
||||
// Run the syncHandler, passing it the namespace/name string of the Pod resource to be synced.
|
||||
if err := q.handler(ctx, key); err != nil {
|
||||
if q.workqueue.NumRequeues(key) < MaxRetries {
|
||||
err := q.handler(ctx, qi.key)
|
||||
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
delete(q.itemsBeingProcessed, qi.key)
|
||||
if qi.forget {
|
||||
q.ratelimiter.Forget(qi.key)
|
||||
log.G(ctx).WithError(err).Warnf("forgetting %q as told to forget while in progress", qi.key)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
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", key)
|
||||
q.workqueue.AddRateLimited(key)
|
||||
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
|
||||
}
|
||||
// We've exceeded the maximum retries, so we must Forget the key.
|
||||
q.workqueue.Forget(key)
|
||||
return pkgerrors.Wrapf(err, "forgetting %q due to maximum retries reached", 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)
|
||||
}
|
||||
}
|
||||
// Finally, if no error occurs we Forget this item so it does not get queued again until another change happens.
|
||||
q.workqueue.Forget(key)
|
||||
|
||||
return nil
|
||||
// We've exceeded the maximum retries or we were successful.
|
||||
q.ratelimiter.Forget(qi.key)
|
||||
if !qi.redirtiedAt.IsZero() {
|
||||
delay := time.Until(qi.redirtiedAt)
|
||||
newQI := q.insert(ctx, qi.key, qi.redirtiedWithRatelimit, &delay)
|
||||
newQI.addedViaRedirty = true
|
||||
}
|
||||
|
||||
span.SetStatus(err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (q *Queue) String() string {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
items := make([]string, 0, q.items.Len())
|
||||
|
||||
for next := q.items.Front(); next != nil; next = next.Next() {
|
||||
items = append(items, next.Value.(*queueItem).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)
|
||||
}
|
||||
|
||||
@@ -5,20 +5,24 @@ import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/log"
|
||||
logruslogger "github.com/virtual-kubelet/virtual-kubelet/log/logrus"
|
||||
"go.uber.org/goleak"
|
||||
"golang.org/x/time/rate"
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"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()
|
||||
@@ -35,15 +39,66 @@ func TestQueueMaxRetries(t *testing.T) {
|
||||
// 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)
|
||||
wq.Enqueue("test")
|
||||
), t.Name(), handler, nil)
|
||||
wq.Enqueue(context.TODO(), "test")
|
||||
|
||||
for n < MaxRetries {
|
||||
assert.Assert(t, wq.handleQueueItem(ctx))
|
||||
}
|
||||
|
||||
assert.Assert(t, is.Equal(n, MaxRetries))
|
||||
assert.Assert(t, is.Equal(0, wq.workqueue.Len()))
|
||||
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.BucketRateLimiter{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) {
|
||||
@@ -51,65 +106,405 @@ func TestForget(t *testing.T) {
|
||||
handler := func(ctx context.Context, key string) error {
|
||||
panic("Should never be called")
|
||||
}
|
||||
wq := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), handler)
|
||||
wq := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), handler, nil)
|
||||
|
||||
wq.Forget("val")
|
||||
assert.Assert(t, is.Equal(0, wq.workqueue.Len()))
|
||||
wq.Forget(context.TODO(), "val")
|
||||
assert.Assert(t, is.Equal(0, wq.Len()))
|
||||
|
||||
v := "test"
|
||||
wq.EnqueueWithoutRateLimit(v)
|
||||
assert.Assert(t, is.Equal(1, wq.workqueue.Len()))
|
||||
|
||||
t.Skip("This is broken")
|
||||
// Workqueue docs:
|
||||
// Forget indicates that an item is finished being retried. Doesn't matter whether it's for perm failing
|
||||
// or for success, we'll stop the rate limiter from tracking it. This only clears the `rateLimiter`, you
|
||||
// still have to call `Done` on the queue.
|
||||
// Even if you do this, it doesn't work: https://play.golang.com/p/8vfL_RCsFGI
|
||||
assert.Assert(t, is.Equal(0, wq.workqueue.Len()))
|
||||
|
||||
wq.EnqueueWithoutRateLimit(context.TODO(), v)
|
||||
assert.Assert(t, is.Equal(1, wq.Len()))
|
||||
}
|
||||
|
||||
func TestQueueTerminate(t *testing.T) {
|
||||
func TestQueueEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
defer goleak.VerifyNone(t,
|
||||
// Ignore existing goroutines
|
||||
goleak.IgnoreCurrent(),
|
||||
// Ignore klog background flushers
|
||||
goleak.IgnoreTopFunction("k8s.io/klog.(*loggingT).flushDaemon"),
|
||||
goleak.IgnoreTopFunction("k8s.io/klog/v2.(*loggingT).flushDaemon"),
|
||||
// Workqueue runs a goroutine in the background to handle background functions. AFAICT, they're unkillable
|
||||
// and are designed to stop after a certain idle window
|
||||
goleak.IgnoreTopFunction("k8s.io/client-go/util/workqueue.(*Type).updateUnfinishedWorkLoop"),
|
||||
goleak.IgnoreTopFunction("k8s.io/client-go/util/workqueue.(*delayingType).waitingLoop"),
|
||||
)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
testMap := &sync.Map{}
|
||||
handler := func(ctx context.Context, key string) error {
|
||||
testMap.Store(key, struct{}{})
|
||||
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
|
||||
return nil
|
||||
}
|
||||
}, nil)
|
||||
|
||||
wq := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), handler)
|
||||
group := &wait.Group{}
|
||||
group.StartWithContext(ctx, func(ctx context.Context) {
|
||||
wq.Run(ctx, 10)
|
||||
item, err := q.getNextItem(ctx)
|
||||
assert.Error(t, err, context.DeadlineExceeded.Error())
|
||||
assert.Assert(t, is.Nil(item))
|
||||
}
|
||||
|
||||
func TestQueueItemNoSleep(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
q.lock.Lock()
|
||||
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)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.Equal(item.key, "foo"))
|
||||
|
||||
item, err = q.getNextItem(ctx)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.Equal(item.key, "bar"))
|
||||
}
|
||||
|
||||
func TestQueueItemSleep(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
|
||||
return nil
|
||||
}, nil)
|
||||
q.lock.Lock()
|
||||
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)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.Equal(item.key, "foo"))
|
||||
}
|
||||
|
||||
func TestQueueBackgroundAdd(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5000*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
q := New(workqueue.DefaultItemBasedRateLimiter(), 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, nil)
|
||||
})
|
||||
for i := 0; i < 1000; i++ {
|
||||
wq.EnqueueWithoutRateLimit(strconv.Itoa(i))
|
||||
|
||||
item, err := q.getNextItem(ctx)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.Equal(item.key, "foo"))
|
||||
assert.Assert(t, time.Since(start) > 100*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestQueueBackgroundAdvance(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5000*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
|
||||
return nil
|
||||
}, nil)
|
||||
start := time.Now()
|
||||
q.lock.Lock()
|
||||
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, nil)
|
||||
})
|
||||
|
||||
item, err := q.getNextItem(ctx)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.Equal(item.key, "foo"))
|
||||
assert.Assert(t, time.Since(start) > 200*time.Millisecond)
|
||||
assert.Assert(t, time.Since(start) < 5*time.Second)
|
||||
}
|
||||
|
||||
func TestQueueRedirty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5000*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
var times int64
|
||||
var q *Queue
|
||||
q = New(workqueue.DefaultItemBasedRateLimiter(), 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")
|
||||
} else {
|
||||
cancel()
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
q.EnqueueWithoutRateLimit(context.TODO(), "foo")
|
||||
q.Run(ctx, 1)
|
||||
for !q.Empty() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
assert.Assert(t, is.Equal(atomic.LoadInt64(×), int64(2)))
|
||||
}
|
||||
|
||||
func TestHeapConcurrency(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5000*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
seen := sync.Map{}
|
||||
q := New(workqueue.DefaultItemBasedRateLimiter(), 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))
|
||||
}
|
||||
|
||||
for wq.workqueue.Len() > 0 {
|
||||
assert.Assert(t, q.Len() == 20)
|
||||
|
||||
go q.Run(ctx, 20)
|
||||
for q.Len() > 0 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
_, ok := testMap.Load(strconv.Itoa(i))
|
||||
assert.Assert(t, ok, "Item %d missing", i)
|
||||
for i := 0; i < 20; i++ {
|
||||
_, ok := seen.Load(strconv.Itoa(i))
|
||||
assert.Assert(t, ok, "Did not observe: %d", i)
|
||||
}
|
||||
assert.Assert(t, time.Since(start) < 5*time.Second)
|
||||
}
|
||||
|
||||
func checkConsistency(t *testing.T, q *Queue) {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
for next := q.items.Front(); next != nil && next.Next() != nil; next = next.Next() {
|
||||
qi := next.Value.(*queueItem)
|
||||
qiNext := next.Next().Value.(*queueItem)
|
||||
assert.Assert(t, qi.plannedToStartWorkAt.Before(qiNext.plannedToStartWorkAt) || qi.plannedToStartWorkAt.Equal(qiNext.plannedToStartWorkAt))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeapOrder(t *testing.T) {
|
||||
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
|
||||
return nil
|
||||
}, nil)
|
||||
q.clock = nonmovingClock{}
|
||||
|
||||
q.EnqueueWithoutRateLimitWithDelay(context.TODO(), "a", 1000)
|
||||
q.EnqueueWithoutRateLimitWithDelay(context.TODO(), "b", 2000)
|
||||
q.EnqueueWithoutRateLimitWithDelay(context.TODO(), "c", 3000)
|
||||
q.EnqueueWithoutRateLimitWithDelay(context.TODO(), "d", 4000)
|
||||
q.EnqueueWithoutRateLimitWithDelay(context.TODO(), "e", 5000)
|
||||
checkConsistency(t, q)
|
||||
t.Logf("%v", q)
|
||||
q.EnqueueWithoutRateLimitWithDelay(context.TODO(), "d", 1000)
|
||||
checkConsistency(t, q)
|
||||
t.Logf("%v", q)
|
||||
q.EnqueueWithoutRateLimitWithDelay(context.TODO(), "c", 1001)
|
||||
checkConsistency(t, q)
|
||||
t.Logf("%v", q)
|
||||
q.EnqueueWithoutRateLimitWithDelay(context.TODO(), "e", 999)
|
||||
checkConsistency(t, q)
|
||||
t.Logf("%v", q)
|
||||
}
|
||||
|
||||
type rateLimitWrapper struct {
|
||||
addedMap sync.Map
|
||||
forgottenMap sync.Map
|
||||
rl workqueue.RateLimiter
|
||||
}
|
||||
|
||||
func (r *rateLimitWrapper) When(item interface{}) time.Duration {
|
||||
if _, ok := r.forgottenMap.Load(item); ok {
|
||||
r.forgottenMap.Delete(item)
|
||||
// Reset the added map
|
||||
r.addedMap.Store(item, 1)
|
||||
} else {
|
||||
actual, loaded := r.addedMap.LoadOrStore(item, 1)
|
||||
if loaded {
|
||||
r.addedMap.Store(item, actual.(int)+1)
|
||||
}
|
||||
}
|
||||
|
||||
cancel()
|
||||
group.Wait()
|
||||
return r.rl.When(item)
|
||||
}
|
||||
|
||||
func (r *rateLimitWrapper) Forget(item interface{}) {
|
||||
r.forgottenMap.Store(item, struct{}{})
|
||||
r.rl.Forget(item)
|
||||
}
|
||||
|
||||
func (r *rateLimitWrapper) NumRequeues(item interface{}) int {
|
||||
return r.rl.NumRequeues(item)
|
||||
}
|
||||
|
||||
func TestRateLimiter(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5000*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
syncMap := sync.Map{}
|
||||
syncMap.Store("foo", 0)
|
||||
syncMap.Store("bar", 0)
|
||||
syncMap.Store("baz", 0)
|
||||
syncMap.Store("quux", 0)
|
||||
|
||||
start := time.Now()
|
||||
ratelimiter := &rateLimitWrapper{
|
||||
rl: workqueue.NewItemFastSlowRateLimiter(1*time.Millisecond, 100*time.Millisecond, 1),
|
||||
}
|
||||
|
||||
q := New(ratelimiter, t.Name(), func(ctx context.Context, key string) error {
|
||||
oldValue, _ := syncMap.Load(key)
|
||||
syncMap.Store(key, oldValue.(int)+1)
|
||||
if oldValue.(int) < 9 {
|
||||
return errors.New("test")
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
enqueued := 0
|
||||
syncMap.Range(func(key, value interface{}) bool {
|
||||
enqueued++
|
||||
q.Enqueue(context.TODO(), key.(string))
|
||||
return true
|
||||
})
|
||||
|
||||
assert.Assert(t, enqueued == 4)
|
||||
go q.Run(ctx, 10)
|
||||
|
||||
incomplete := true
|
||||
for incomplete {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
incomplete = false
|
||||
// Wait for all items to finish processing.
|
||||
syncMap.Range(func(key, value interface{}) bool {
|
||||
if value.(int) < 10 {
|
||||
incomplete = true
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Make sure there were ~9 "slow" rate limits per item, and 1 fast
|
||||
assert.Assert(t, time.Since(start) > 9*100*time.Millisecond)
|
||||
// Make sure we didn't go off the deep end.
|
||||
assert.Assert(t, time.Since(start) < 2*9*100*time.Millisecond)
|
||||
|
||||
// Make sure each item was seen. And Forgotten.
|
||||
syncMap.Range(func(key, value interface{}) bool {
|
||||
_, ok := ratelimiter.forgottenMap.Load(key)
|
||||
assert.Assert(t, ok, "%s in forgotten map", key)
|
||||
val, ok := ratelimiter.addedMap.Load(key)
|
||||
assert.Assert(t, ok, "%s in added map", key)
|
||||
assert.Assert(t, val == 10)
|
||||
return true
|
||||
})
|
||||
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
assert.Assert(t, len(q.itemsInQueue) == 0)
|
||||
assert.Assert(t, len(q.itemsBeingProcessed) == 0)
|
||||
assert.Assert(t, q.items.Len() == 0)
|
||||
|
||||
}
|
||||
|
||||
func TestQueueForgetInProgress(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
var times int64
|
||||
var q *Queue
|
||||
q = New(workqueue.DefaultItemBasedRateLimiter(), 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)
|
||||
for !q.Empty() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
assert.Assert(t, is.Equal(atomic.LoadInt64(×), int64(1)))
|
||||
}
|
||||
|
||||
func TestQueueForgetBeforeStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
q := New(workqueue.DefaultItemBasedRateLimiter(), 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")
|
||||
go q.Run(ctx, 1)
|
||||
for !q.Empty() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueMoveItem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
q := New(workqueue.DefaultItemBasedRateLimiter(), t.Name(), func(ctx context.Context, key string) error {
|
||||
panic("shouldn't be called")
|
||||
}, nil)
|
||||
q.clock = nonmovingClock{}
|
||||
|
||||
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, durationPtr(2000))
|
||||
checkConsistency(t, q)
|
||||
t.Log(q)
|
||||
|
||||
q.insert(ctx, "foo", false, durationPtr(1999))
|
||||
checkConsistency(t, q)
|
||||
t.Log(q)
|
||||
|
||||
q.insert(ctx, "foo", false, durationPtr(999))
|
||||
checkConsistency(t, q)
|
||||
t.Log(q)
|
||||
}
|
||||
|
||||
type nonmovingClock struct {
|
||||
}
|
||||
|
||||
func (n nonmovingClock) Now() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (n nonmovingClock) Since(t time.Time) time.Duration {
|
||||
return n.Now().Sub(t)
|
||||
}
|
||||
|
||||
func (n nonmovingClock) After(d time.Duration) <-chan time.Time {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (n nonmovingClock) NewTimer(d time.Duration) clock.Timer {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (n nonmovingClock) Sleep(d time.Duration) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (n nonmovingClock) Tick(d time.Duration) <-chan time.Time {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
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,10 +101,20 @@ 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 *v1.Pod) bool {
|
||||
for _, cond := range pod.Status.Conditions {
|
||||
if cond.Type == v1.PodReady && cond.Status == v1.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) {
|
||||
|
||||
@@ -3,10 +3,10 @@ package framework
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
api "github.com/virtual-kubelet/virtual-kubelet/node/api"
|
||||
stats "github.com/virtual-kubelet/virtual-kubelet/node/api/statsv1alpha1"
|
||||
"k8s.io/apimachinery/pkg/util/net"
|
||||
stats "k8s.io/kubernetes/pkg/kubelet/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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
139
node/api/attach.go
Normal file
139
node/api/attach.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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")
|
||||
}
|
||||
|
||||
@@ -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.FmtText)
|
||||
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/ioutil"
|
||||
"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 := ioutil.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) //nolint:golint
|
||||
|
||||
func HandleRunningPods(getPods PodListerFunc) http.HandlerFunc { // nolint:golint
|
||||
func HandleRunningPods(getPods PodListerFunc) http.HandlerFunc { //nolint:golint
|
||||
if getPods == nil {
|
||||
return NotImplemented
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
/* #nosec */
|
||||
v1.SchemeBuilder.AddToScheme(scheme) //nolint:errcheck
|
||||
codecs := serializer.NewCodecFactory(scheme)
|
||||
|
||||
|
||||
@@ -33,18 +33,22 @@ 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
|
||||
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()
|
||||
@@ -65,6 +69,14 @@ 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")
|
||||
|
||||
if p.GetStatsSummary != nil {
|
||||
f := HandlePodStatsSummary(p.GetStatsSummary)
|
||||
@@ -72,6 +84,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 +96,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 +114,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 +148,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 +158,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"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/node/api/statsv1alpha1"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
|
||||
7
node/api/statsv1alpha1/README.md
Normal file
7
node/api/statsv1alpha1/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
These types are copied from the [k8s.io/kubelet](https://pkg.go.dev/k8s.io/kubelet@v0.21.0/pkg/apis/stats/v1alpha1) module.
|
||||
They are used from a type alias in the API package.
|
||||
|
||||
It is being used this way because the module is only available from 1.20 and on, but currently we are pinned to v1.19 and plan to continue to support v1.19 for some time.
|
||||
Likewise we want to stop importing k8s.io/kubernetes (where the older type def is) since this transatively imports all of kubernetes.
|
||||
|
||||
After the min version is v1.20 we can update the type alias to point the the module and remove these type definitions.
|
||||
345
node/api/statsv1alpha1/types.go
Normal file
345
node/api/statsv1alpha1/types.go
Normal file
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
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 statsv1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Summary is a top-level container for holding NodeStats and PodStats.
|
||||
type Summary struct {
|
||||
// Overall node stats.
|
||||
Node NodeStats `json:"node"`
|
||||
// Per-pod stats.
|
||||
Pods []PodStats `json:"pods"`
|
||||
}
|
||||
|
||||
// NodeStats holds node-level unprocessed sample stats.
|
||||
type NodeStats struct {
|
||||
// Reference to the measured Node.
|
||||
NodeName string `json:"nodeName"`
|
||||
// Stats of system daemons tracked as raw containers.
|
||||
// The system containers are named according to the SystemContainer* constants.
|
||||
// +optional
|
||||
// +patchMergeKey=name
|
||||
// +patchStrategy=merge
|
||||
SystemContainers []ContainerStats `json:"systemContainers,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
|
||||
// The time at which data collection for the node-scoped (i.e. aggregate) stats was (re)started.
|
||||
StartTime metav1.Time `json:"startTime"`
|
||||
// Stats pertaining to CPU resources.
|
||||
// +optional
|
||||
CPU *CPUStats `json:"cpu,omitempty"`
|
||||
// Stats pertaining to memory (RAM) resources.
|
||||
// +optional
|
||||
Memory *MemoryStats `json:"memory,omitempty"`
|
||||
// Stats pertaining to network resources.
|
||||
// +optional
|
||||
Network *NetworkStats `json:"network,omitempty"`
|
||||
// Stats pertaining to total usage of filesystem resources on the rootfs used by node k8s components.
|
||||
// NodeFs.Used is the total bytes used on the filesystem.
|
||||
// +optional
|
||||
Fs *FsStats `json:"fs,omitempty"`
|
||||
// Stats about the underlying container runtime.
|
||||
// +optional
|
||||
Runtime *RuntimeStats `json:"runtime,omitempty"`
|
||||
// Stats about the rlimit of system.
|
||||
// +optional
|
||||
Rlimit *RlimitStats `json:"rlimit,omitempty"`
|
||||
}
|
||||
|
||||
// RlimitStats are stats rlimit of OS.
|
||||
type RlimitStats struct {
|
||||
Time metav1.Time `json:"time"`
|
||||
|
||||
// The max PID of OS.
|
||||
MaxPID *int64 `json:"maxpid,omitempty"`
|
||||
// The number of running process in the OS.
|
||||
NumOfRunningProcesses *int64 `json:"curproc,omitempty"`
|
||||
}
|
||||
|
||||
// RuntimeStats are stats pertaining to the underlying container runtime.
|
||||
type RuntimeStats struct {
|
||||
// Stats about the underlying filesystem where container images are stored.
|
||||
// This filesystem could be the same as the primary (root) filesystem.
|
||||
// Usage here refers to the total number of bytes occupied by images on the filesystem.
|
||||
// +optional
|
||||
ImageFs *FsStats `json:"imageFs,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
// SystemContainerKubelet is the container name for the system container tracking Kubelet usage.
|
||||
SystemContainerKubelet = "kubelet"
|
||||
// SystemContainerRuntime is the container name for the system container tracking the runtime (e.g. docker) usage.
|
||||
SystemContainerRuntime = "runtime"
|
||||
// SystemContainerMisc is the container name for the system container tracking non-kubernetes processes.
|
||||
SystemContainerMisc = "misc"
|
||||
// SystemContainerPods is the container name for the system container tracking user pods.
|
||||
SystemContainerPods = "pods"
|
||||
)
|
||||
|
||||
// ProcessStats are stats pertaining to processes.
|
||||
type ProcessStats struct {
|
||||
// Number of processes
|
||||
// +optional
|
||||
ProcessCount *uint64 `json:"process_count,omitempty"`
|
||||
}
|
||||
|
||||
// PodStats holds pod-level unprocessed sample stats.
|
||||
type PodStats struct {
|
||||
// Reference to the measured Pod.
|
||||
PodRef PodReference `json:"podRef"`
|
||||
// The time at which data collection for the pod-scoped (e.g. network) stats was (re)started.
|
||||
StartTime metav1.Time `json:"startTime"`
|
||||
// Stats of containers in the measured pod.
|
||||
// +patchMergeKey=name
|
||||
// +patchStrategy=merge
|
||||
Containers []ContainerStats `json:"containers" patchStrategy:"merge" patchMergeKey:"name"`
|
||||
// Stats pertaining to CPU resources consumed by pod cgroup (which includes all containers' resource usage and pod overhead).
|
||||
// +optional
|
||||
CPU *CPUStats `json:"cpu,omitempty"`
|
||||
// Stats pertaining to memory (RAM) resources consumed by pod cgroup (which includes all containers' resource usage and pod overhead).
|
||||
// +optional
|
||||
Memory *MemoryStats `json:"memory,omitempty"`
|
||||
// Stats pertaining to network resources.
|
||||
// +optional
|
||||
Network *NetworkStats `json:"network,omitempty"`
|
||||
// Stats pertaining to volume usage of filesystem resources.
|
||||
// VolumeStats.UsedBytes is the number of bytes used by the Volume
|
||||
// +optional
|
||||
// +patchMergeKey=name
|
||||
// +patchStrategy=merge
|
||||
VolumeStats []VolumeStats `json:"volume,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
|
||||
// EphemeralStorage reports the total filesystem usage for the containers and emptyDir-backed volumes in the measured Pod.
|
||||
// +optional
|
||||
EphemeralStorage *FsStats `json:"ephemeral-storage,omitempty"`
|
||||
// ProcessStats pertaining to processes.
|
||||
// +optional
|
||||
ProcessStats *ProcessStats `json:"process_stats,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerStats holds container-level unprocessed sample stats.
|
||||
type ContainerStats struct {
|
||||
// Reference to the measured container.
|
||||
Name string `json:"name"`
|
||||
// The time at which data collection for this container was (re)started.
|
||||
StartTime metav1.Time `json:"startTime"`
|
||||
// Stats pertaining to CPU resources.
|
||||
// +optional
|
||||
CPU *CPUStats `json:"cpu,omitempty"`
|
||||
// Stats pertaining to memory (RAM) resources.
|
||||
// +optional
|
||||
Memory *MemoryStats `json:"memory,omitempty"`
|
||||
// Metrics for Accelerators. Each Accelerator corresponds to one element in the array.
|
||||
Accelerators []AcceleratorStats `json:"accelerators,omitempty"`
|
||||
// Stats pertaining to container rootfs usage of filesystem resources.
|
||||
// Rootfs.UsedBytes is the number of bytes used for the container write layer.
|
||||
// +optional
|
||||
Rootfs *FsStats `json:"rootfs,omitempty"`
|
||||
// Stats pertaining to container logs usage of filesystem resources.
|
||||
// Logs.UsedBytes is the number of bytes used for the container logs.
|
||||
// +optional
|
||||
Logs *FsStats `json:"logs,omitempty"`
|
||||
// User defined metrics that are exposed by containers in the pod. Typically, we expect only one container in the pod to be exposing user defined metrics. In the event of multiple containers exposing metrics, they will be combined here.
|
||||
// +patchMergeKey=name
|
||||
// +patchStrategy=merge
|
||||
UserDefinedMetrics []UserDefinedMetric `json:"userDefinedMetrics,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
|
||||
}
|
||||
|
||||
// PodReference contains enough information to locate the referenced pod.
|
||||
type PodReference struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
UID string `json:"uid"`
|
||||
}
|
||||
|
||||
// InterfaceStats contains resource value data about interface.
|
||||
type InterfaceStats struct {
|
||||
// The name of the interface
|
||||
Name string `json:"name"`
|
||||
// Cumulative count of bytes received.
|
||||
// +optional
|
||||
RxBytes *uint64 `json:"rxBytes,omitempty"`
|
||||
// Cumulative count of receive errors encountered.
|
||||
// +optional
|
||||
RxErrors *uint64 `json:"rxErrors,omitempty"`
|
||||
// Cumulative count of bytes transmitted.
|
||||
// +optional
|
||||
TxBytes *uint64 `json:"txBytes,omitempty"`
|
||||
// Cumulative count of transmit errors encountered.
|
||||
// +optional
|
||||
TxErrors *uint64 `json:"txErrors,omitempty"`
|
||||
}
|
||||
|
||||
// NetworkStats contains data about network resources.
|
||||
type NetworkStats struct {
|
||||
// The time at which these stats were updated.
|
||||
Time metav1.Time `json:"time"`
|
||||
|
||||
// Stats for the default interface, if found
|
||||
InterfaceStats `json:",inline"`
|
||||
|
||||
Interfaces []InterfaceStats `json:"interfaces,omitempty"`
|
||||
}
|
||||
|
||||
// CPUStats contains data about CPU usage.
|
||||
type CPUStats struct {
|
||||
// The time at which these stats were updated.
|
||||
Time metav1.Time `json:"time"`
|
||||
// Total CPU usage (sum of all cores) averaged over the sample window.
|
||||
// The "core" unit can be interpreted as CPU core-nanoseconds per second.
|
||||
// +optional
|
||||
UsageNanoCores *uint64 `json:"usageNanoCores,omitempty"`
|
||||
// Cumulative CPU usage (sum of all cores) since object creation.
|
||||
// +optional
|
||||
UsageCoreNanoSeconds *uint64 `json:"usageCoreNanoSeconds,omitempty"`
|
||||
}
|
||||
|
||||
// MemoryStats contains data about memory usage.
|
||||
type MemoryStats struct {
|
||||
// The time at which these stats were updated.
|
||||
Time metav1.Time `json:"time"`
|
||||
// Available memory for use. This is defined as the memory limit - workingSetBytes.
|
||||
// If memory limit is undefined, the available bytes is omitted.
|
||||
// +optional
|
||||
AvailableBytes *uint64 `json:"availableBytes,omitempty"`
|
||||
// Total memory in use. This includes all memory regardless of when it was accessed.
|
||||
// +optional
|
||||
UsageBytes *uint64 `json:"usageBytes,omitempty"`
|
||||
// The amount of working set memory. This includes recently accessed memory,
|
||||
// dirty memory, and kernel memory. WorkingSetBytes is <= UsageBytes
|
||||
// +optional
|
||||
WorkingSetBytes *uint64 `json:"workingSetBytes,omitempty"`
|
||||
// The amount of anonymous and swap cache memory (includes transparent
|
||||
// hugepages).
|
||||
// +optional
|
||||
RSSBytes *uint64 `json:"rssBytes,omitempty"`
|
||||
// Cumulative number of minor page faults.
|
||||
// +optional
|
||||
PageFaults *uint64 `json:"pageFaults,omitempty"`
|
||||
// Cumulative number of major page faults.
|
||||
// +optional
|
||||
MajorPageFaults *uint64 `json:"majorPageFaults,omitempty"`
|
||||
}
|
||||
|
||||
// AcceleratorStats contains stats for accelerators attached to the container.
|
||||
type AcceleratorStats struct {
|
||||
// Make of the accelerator (nvidia, amd, google etc.)
|
||||
Make string `json:"make"`
|
||||
|
||||
// Model of the accelerator (tesla-p100, tesla-k80 etc.)
|
||||
Model string `json:"model"`
|
||||
|
||||
// ID of the accelerator.
|
||||
ID string `json:"id"`
|
||||
|
||||
// Total accelerator memory.
|
||||
// unit: bytes
|
||||
MemoryTotal uint64 `json:"memoryTotal"`
|
||||
|
||||
// Total accelerator memory allocated.
|
||||
// unit: bytes
|
||||
MemoryUsed uint64 `json:"memoryUsed"`
|
||||
|
||||
// Percent of time over the past sample period (10s) during which
|
||||
// the accelerator was actively processing.
|
||||
DutyCycle uint64 `json:"dutyCycle"`
|
||||
}
|
||||
|
||||
// VolumeStats contains data about Volume filesystem usage.
|
||||
type VolumeStats struct {
|
||||
// Embedded FsStats
|
||||
FsStats `json:",inline"`
|
||||
// Name is the name given to the Volume
|
||||
// +optional
|
||||
Name string `json:"name,omitempty"`
|
||||
// Reference to the PVC, if one exists
|
||||
// +optional
|
||||
PVCRef *PVCReference `json:"pvcRef,omitempty"`
|
||||
}
|
||||
|
||||
// PVCReference contains enough information to describe the referenced PVC.
|
||||
type PVCReference struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
// FsStats contains data about filesystem usage.
|
||||
type FsStats struct {
|
||||
// The time at which these stats were updated.
|
||||
Time metav1.Time `json:"time"`
|
||||
// AvailableBytes represents the storage space available (bytes) for the filesystem.
|
||||
// +optional
|
||||
AvailableBytes *uint64 `json:"availableBytes,omitempty"`
|
||||
// CapacityBytes represents the total capacity (bytes) of the filesystems underlying storage.
|
||||
// +optional
|
||||
CapacityBytes *uint64 `json:"capacityBytes,omitempty"`
|
||||
// UsedBytes represents the bytes used for a specific task on the filesystem.
|
||||
// This may differ from the total bytes used on the filesystem and may not equal CapacityBytes - AvailableBytes.
|
||||
// e.g. For ContainerStats.Rootfs this is the bytes used by the container rootfs on the filesystem.
|
||||
// +optional
|
||||
UsedBytes *uint64 `json:"usedBytes,omitempty"`
|
||||
// InodesFree represents the free inodes in the filesystem.
|
||||
// +optional
|
||||
InodesFree *uint64 `json:"inodesFree,omitempty"`
|
||||
// Inodes represents the total inodes in the filesystem.
|
||||
// +optional
|
||||
Inodes *uint64 `json:"inodes,omitempty"`
|
||||
// InodesUsed represents the inodes used by the filesystem
|
||||
// This may not equal Inodes - InodesFree because this filesystem may share inodes with other "filesystems"
|
||||
// e.g. For ContainerStats.Rootfs, this is the inodes used only by that container, and does not count inodes used by other containers.
|
||||
InodesUsed *uint64 `json:"inodesUsed,omitempty"`
|
||||
}
|
||||
|
||||
// UserDefinedMetricType defines how the metric should be interpreted by the user.
|
||||
type UserDefinedMetricType string
|
||||
|
||||
const (
|
||||
// MetricGauge is an instantaneous value. May increase or decrease.
|
||||
MetricGauge UserDefinedMetricType = "gauge"
|
||||
|
||||
// MetricCumulative is a counter-like value that is only expected to increase.
|
||||
MetricCumulative UserDefinedMetricType = "cumulative"
|
||||
|
||||
// MetricDelta is a rate over a time period.
|
||||
MetricDelta UserDefinedMetricType = "delta"
|
||||
)
|
||||
|
||||
// UserDefinedMetricDescriptor contains metadata that describes a user defined metric.
|
||||
type UserDefinedMetricDescriptor struct {
|
||||
// The name of the metric.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Type of the metric.
|
||||
Type UserDefinedMetricType `json:"type"`
|
||||
|
||||
// Display Units for the stats.
|
||||
Units string `json:"units"`
|
||||
|
||||
// Metadata labels associated with this metric.
|
||||
// +optional
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
// UserDefinedMetric represents a metric defined and generated by users.
|
||||
type UserDefinedMetric struct {
|
||||
UserDefinedMetricDescriptor `json:",inline"`
|
||||
// The time at which these stats were updated.
|
||||
Time metav1.Time `json:"time"`
|
||||
// Value of the metric. Float64s have 53 bit precision.
|
||||
// We do not foresee any metrics exceeding that value.
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
110
node/env_test.go
110
node/env_test.go
@@ -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"
|
||||
@@ -16,8 +16,9 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -25,6 +26,9 @@ func TestEnvtest(t *testing.T) {
|
||||
if !*enableEnvTest || os.Getenv("VK_ENVTEST") != "" {
|
||||
t.Skip("test only runs when -envtest is passed or if VK_ENVTEST is set to a non-empty value")
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
env := &envtest.Environment{}
|
||||
_, err := env.Start()
|
||||
assert.NilError(t, err)
|
||||
@@ -33,15 +37,17 @@ func TestEnvtest(t *testing.T) {
|
||||
}()
|
||||
|
||||
t.Log("Env test environment ready")
|
||||
t.Run("E2ERunWithoutLeases", func(t *testing.T) {
|
||||
t.Run("E2ERunWithoutLeases", wrapE2ETest(ctx, env, func(ctx context.Context, t *testing.T, environment *envtest.Environment) {
|
||||
testNodeE2ERun(t, env, false)
|
||||
})
|
||||
t.Run("E2ERunWithLeases", func(t *testing.T) {
|
||||
}))
|
||||
t.Run("E2ERunWithLeases", wrapE2ETest(ctx, env, func(ctx context.Context, t *testing.T, environment *envtest.Environment) {
|
||||
testNodeE2ERun(t, env, true)
|
||||
})
|
||||
}))
|
||||
|
||||
t.Run("E2EPodStatusUpdate", wrapE2ETest(ctx, env, testPodStatusUpdate))
|
||||
}
|
||||
|
||||
func nodeNameForTest(t *testing.T) string {
|
||||
func kubernetesNameForTest(t *testing.T) string {
|
||||
name := t.Name()
|
||||
name = strings.ToLower(name)
|
||||
name = strings.ReplaceAll(name, "/", "-")
|
||||
@@ -49,17 +55,91 @@ func nodeNameForTest(t *testing.T) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func wrapE2ETest(ctx context.Context, env *envtest.Environment, f func(context.Context, *testing.T, *envtest.Environment)) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
log.G(ctx)
|
||||
sl := logrus.StandardLogger()
|
||||
sl.SetLevel(logrus.DebugLevel)
|
||||
logger := logruslogger.FromLogrus(sl.WithField("test", t.Name()))
|
||||
ctx = log.WithLogger(ctx, logger)
|
||||
|
||||
// The following requires that E2E tests are performed *sequentially*
|
||||
log.L = logger
|
||||
klog.SetLogger(logrusr.New(sl))
|
||||
f(ctx, t, env)
|
||||
}
|
||||
}
|
||||
|
||||
func testPodStatusUpdate(ctx context.Context, t *testing.T, env *envtest.Environment) {
|
||||
provider := newMockProvider()
|
||||
|
||||
clientset, err := kubernetes.NewForConfig(env.Config)
|
||||
assert.NilError(t, err)
|
||||
pods := clientset.CoreV1().Pods(testNamespace)
|
||||
|
||||
assert.NilError(t, wireUpSystemWithClient(ctx, provider, clientset, func(ctx context.Context, s *system) {
|
||||
p := newPod(forRealAPIServer, nameBasedOnTest(t))
|
||||
// In real API server, we don't set the resource version
|
||||
p.ResourceVersion = ""
|
||||
newPod, err := pods.Create(ctx, p, metav1.CreateOptions{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
key, err := buildKey(newPod)
|
||||
assert.NilError(t, err)
|
||||
|
||||
listOptions := metav1.ListOptions{
|
||||
FieldSelector: fields.OneTermEqualSelector("metadata.name", p.ObjectMeta.Name).String(),
|
||||
}
|
||||
|
||||
// Setup a watch to check if the pod is in running
|
||||
watcher, err := s.client.CoreV1().Pods(testNamespace).Watch(ctx, listOptions)
|
||||
assert.NilError(t, err)
|
||||
defer watcher.Stop()
|
||||
// Start the pod controller
|
||||
assert.NilError(t, s.start(ctx))
|
||||
var serverPod *corev1.Pod
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Context ended early: %s", ctx.Err().Error())
|
||||
case ev := <-watcher.ResultChan():
|
||||
serverPod = ev.Object.(*corev1.Pod)
|
||||
if serverPod.Status.Phase == corev1.PodRunning {
|
||||
goto running
|
||||
}
|
||||
}
|
||||
}
|
||||
running:
|
||||
t.Log("Observed pod in running state")
|
||||
|
||||
providerPod, ok := provider.pods.Load(key)
|
||||
assert.Assert(t, ok)
|
||||
providerPodCopy := providerPod.(*corev1.Pod).DeepCopy()
|
||||
providerPodCopy.Status = serverPod.Status
|
||||
if providerPodCopy.Annotations == nil {
|
||||
providerPodCopy.Annotations = make(map[string]string, 1)
|
||||
}
|
||||
providerPodCopy.Annotations["testannotation"] = "testvalue"
|
||||
provider.notifier(providerPodCopy)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Context ended early: %s", ctx.Err().Error())
|
||||
case ev := <-watcher.ResultChan():
|
||||
annotations := ev.Object.(*corev1.Pod).Annotations
|
||||
if annotations != nil && annotations["testannotation"] == "testvalue" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func testNodeE2ERun(t *testing.T, env *envtest.Environment, withLeases bool) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
sl := logrus.StandardLogger()
|
||||
sl.SetLevel(logrus.DebugLevel)
|
||||
logger := logruslogger.FromLogrus(sl.WithField("test", t.Name()))
|
||||
ctx = log.WithLogger(ctx, logger)
|
||||
log.L = logger
|
||||
klogv2.SetLogger(logrusr.NewLogger(sl))
|
||||
|
||||
clientset, err := kubernetes.NewForConfig(env.Config)
|
||||
assert.NilError(t, err)
|
||||
nodes := clientset.CoreV1().Nodes()
|
||||
@@ -70,7 +150,7 @@ func testNodeE2ERun(t *testing.T, env *envtest.Environment, withLeases bool) {
|
||||
|
||||
testNode := &corev1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: nodeNameForTest(t),
|
||||
Name: kubernetesNameForTest(t),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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: pointer.String(node.Name),
|
||||
LeaseDurationSeconds: pointer.Int32(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
|
||||
|
||||
@@ -21,13 +21,14 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
ktesting "k8s.io/client-go/testing"
|
||||
"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 (
|
||||
@@ -37,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
|
||||
)
|
||||
@@ -226,12 +227,12 @@ func TestPodLifecycle(t *testing.T) {
|
||||
type testFunction func(ctx context.Context, s *system)
|
||||
type system struct {
|
||||
pc *PodController
|
||||
client *fake.Clientset
|
||||
client kubernetes.Interface
|
||||
podControllerConfig PodControllerConfig
|
||||
}
|
||||
|
||||
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():
|
||||
@@ -262,6 +263,13 @@ func wireUpSystem(ctx context.Context, provider PodLifecycleHandler, f testFunct
|
||||
return false, nil, nil
|
||||
})
|
||||
|
||||
return wireUpSystemWithClient(ctx, provider, client, f)
|
||||
}
|
||||
|
||||
func wireUpSystemWithClient(ctx context.Context, provider PodLifecycleHandler, client kubernetes.Interface, f testFunction) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// This is largely copy and pasted code from the root command
|
||||
sharedInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(
|
||||
client,
|
||||
@@ -359,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()
|
||||
|
||||
@@ -382,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())
|
||||
@@ -428,7 +444,7 @@ func testCreateStartDeleteScenario(ctx context.Context, t *testing.T, s *system,
|
||||
return pod.Name == p.ObjectMeta.Name, nil
|
||||
})
|
||||
|
||||
watchErrCh <- watchErr
|
||||
sendErr(ctx, watchErrCh, watchErr)
|
||||
}()
|
||||
|
||||
// Create the Pod
|
||||
@@ -457,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))
|
||||
@@ -479,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
|
||||
@@ -487,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
|
||||
@@ -551,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,6 +636,17 @@ func randomizeName(pod *corev1.Pod) {
|
||||
pod.Name = name
|
||||
}
|
||||
|
||||
func forRealAPIServer(pod *corev1.Pod) {
|
||||
pod.ResourceVersion = ""
|
||||
pod.ObjectMeta.UID = ""
|
||||
}
|
||||
|
||||
func nameBasedOnTest(t *testing.T) podModifier {
|
||||
return func(pod *corev1.Pod) {
|
||||
pod.Name = kubernetesNameForTest(t)
|
||||
}
|
||||
}
|
||||
|
||||
func newPod(podmodifiers ...podModifier) *corev1.Pod {
|
||||
var terminationGracePeriodSeconds int64 = 30
|
||||
pod := &corev1.Pod{
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
49
node/node.go
49
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
|
||||
@@ -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()
|
||||
@@ -307,14 +336,12 @@ func (n *NodeController) ensureNode(ctx context.Context, providerNode *corev1.No
|
||||
|
||||
// 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
|
||||
@@ -355,6 +382,7 @@ func (n *NodeController) controlLoop(ctx context.Context, providerNode *corev1.N
|
||||
return false
|
||||
}
|
||||
|
||||
close(n.chReady)
|
||||
for {
|
||||
shouldTerminate := loop()
|
||||
if shouldTerminate {
|
||||
@@ -657,10 +685,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),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -65,16 +65,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 +100,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 +149,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 +189,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 +198,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 +212,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 +295,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 +309,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 +357,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()
|
||||
@@ -427,16 +421,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()
|
||||
|
||||
221
node/nodeutil/auth.go
Normal file
221
node/nodeutil/auth.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package nodeutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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(),
|
||||
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)
|
||||
|
||||
info, ok, err := auth.AuthenticateRequest(r)
|
||||
if err != nil || !ok {
|
||||
log.G(r.Context()).WithError(err).Error("Authorization error")
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
logger := log.G(ctx).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)
|
||||
return
|
||||
}
|
||||
|
||||
if decision != authorizer.DecisionAllow {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
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"
|
||||
}
|
||||
438
node/nodeutil/controller.go
Normal file
438
node/nodeutil/controller.go
Normal file
@@ -0,0 +1,438 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 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
|
||||
|
||||
routeAttacher func(Provider, NodeConfig, corev1listers.PodLister)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
if cfg.Client == nil {
|
||||
return nil, errors.New("no client provided")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
nc, err := node.NewNodeController(
|
||||
np,
|
||||
&cfg.NodeSpec,
|
||||
cfg.Client.CoreV1().Nodes(),
|
||||
node.WithNodeEnableLeaseV1(NodeLeaseV1Client(cfg.Client), node.DefaultLeaseDuration),
|
||||
)
|
||||
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,
|
||||
})
|
||||
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,
|
||||
}, 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
|
||||
}
|
||||
80
node/nodeutil/provider.go
Normal file
80
node/nodeutil/provider.go
Normal file
@@ -0,0 +1,80 @@
|
||||
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"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/node/api/statsv1alpha1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
corev1listers "k8s.io/client-go/listers/core/v1"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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,
|
||||
}, true))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
83
node/nodeutil/tls.go
Normal file
83
node/nodeutil/tls.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package nodeutil
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
tlsCfg := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
PreferServerCipherSuites: true,
|
||||
CipherSuites: DefaultServerCiphers(),
|
||||
ClientAuth: tls.RequestClientCert,
|
||||
}
|
||||
for _, o := range opts {
|
||||
if err := o(tlsCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cfg.TLSConfig = tlsCfg
|
||||
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 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.ClientCAs == 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,
|
||||
}
|
||||
}
|
||||
39
node/pod.go
39
node/pod.go
@@ -17,12 +17,14 @@ package node
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"
|
||||
@@ -214,14 +216,7 @@ func (pc *PodController) updatePodStatus(ctx context.Context, podFromKubernetes
|
||||
}
|
||||
kPod := obj.(*knownPod)
|
||||
kPod.Lock()
|
||||
|
||||
podFromProvider := kPod.lastPodStatusReceivedFromProvider.DeepCopy()
|
||||
if cmp.Equal(podFromKubernetes.Status, podFromProvider.Status) && podFromProvider.DeletionTimestamp == nil {
|
||||
kPod.lastPodStatusUpdateSkipped = true
|
||||
kPod.Unlock()
|
||||
return nil
|
||||
}
|
||||
kPod.lastPodStatusUpdateSkipped = false
|
||||
kPod.Unlock()
|
||||
// Pod deleted by provider due some reasons. e.g. a K8s provider, pod created by deployment would be evicted when node is not ready.
|
||||
// If we do not delete pod in K8s, deployment would not create a new one.
|
||||
@@ -325,12 +320,14 @@ func (pc *PodController) enqueuePodStatusUpdate(ctx context.Context, pod *corev1
|
||||
kpod := obj.(*knownPod)
|
||||
kpod.Lock()
|
||||
if cmp.Equal(kpod.lastPodStatusReceivedFromProvider, pod) {
|
||||
kpod.lastPodStatusUpdateSkipped = true
|
||||
kpod.Unlock()
|
||||
return
|
||||
}
|
||||
kpod.lastPodStatusUpdateSkipped = false
|
||||
kpod.lastPodStatusReceivedFromProvider = pod
|
||||
kpod.Unlock()
|
||||
pc.syncPodStatusFromProvider.Enqueue(key)
|
||||
pc.syncPodStatusFromProvider.Enqueue(ctx, key)
|
||||
}
|
||||
|
||||
func (pc *PodController) syncPodStatusFromProviderHandler(ctx context.Context, key string) (retErr error) {
|
||||
@@ -367,7 +364,8 @@ func (pc *PodController) deletePodsFromKubernetesHandler(ctx context.Context, ke
|
||||
ctx, span := trace.StartSpan(ctx, "deletePodsFromKubernetesHandler")
|
||||
defer span.End()
|
||||
|
||||
namespace, name, err := cache.SplitMetaNamespaceKey(key)
|
||||
uid, metaKey := getUIDAndMetaNamespaceKey(key)
|
||||
namespace, name, err := cache.SplitMetaNamespaceKey(metaKey)
|
||||
ctx = span.WithFields(ctx, log.Fields{
|
||||
"namespace": namespace,
|
||||
"name": name,
|
||||
@@ -397,15 +395,25 @@ func (pc *PodController) deletePodsFromKubernetesHandler(ctx context.Context, ke
|
||||
span.SetStatus(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if string(k8sPod.UID) != uid {
|
||||
log.G(ctx).WithField("k8sPodUID", k8sPod.UID).WithField("uid", uid).Warn("Not deleting pod because remote pod has different UID")
|
||||
return nil
|
||||
}
|
||||
if running(&k8sPod.Status) {
|
||||
log.G(ctx).Error("Force deleting pod in running state")
|
||||
}
|
||||
|
||||
// We don't check with the provider before doing this delete. At this point, even if an outstanding pod status update
|
||||
// was in progress,
|
||||
err = pc.client.Pods(namespace).Delete(ctx, name, *metav1.NewDeleteOptions(0))
|
||||
deleteOptions := metav1.NewDeleteOptions(0)
|
||||
deleteOptions.Preconditions = metav1.NewUIDPreconditions(uid)
|
||||
err = pc.client.Pods(namespace).Delete(ctx, name, *deleteOptions)
|
||||
if errors.IsNotFound(err) {
|
||||
log.G(ctx).Warnf("Not deleting pod because %v", err)
|
||||
return nil
|
||||
}
|
||||
if errors.IsConflict(err) {
|
||||
log.G(ctx).Warnf("There was a conflict, maybe trying to delete a Pod that has been recreated: %v", err)
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
@@ -414,3 +422,10 @@ func (pc *PodController) deletePodsFromKubernetesHandler(ctx context.Context, ke
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUIDAndMetaNamespaceKey(key string) (string, string) {
|
||||
idx := strings.LastIndex(key, "/")
|
||||
uid := key[idx+1:]
|
||||
metaKey := key[:idx]
|
||||
return uid, metaKey
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user