Kubectl
January 14, 2024
Kubectl is the all powerful command-line tool that you can use to manage Kubernetes resources and cluster- everything from creating, deleting resources, switching contexts, accessing containers- and more!
Not sure how to pronounce kubectl
? Check out kubectl: The definitive pronunciation guide. I’m a monster and pronounce it “kube-see-tee-l”, but I also pronounce systemctl
as “system-see-tee-l” so at least I’m consistent. Hopefully this also inspires you to just set up the k
alias and never think about the etymology of cli tools again.
Anyway…
There’s a small bug I picked up to fix how kubectl handles in-cluster configuration a.k.a. Issue 93474.
In order to figure out what was actually happening, I learned a couple neat kubectl
tricks.
First, --v
is not just verbose. You can actually set the log level and get a bunch of interesting outputs. Here’s the kubectl
command with no client side configuration working:
I have no name!@kubectl-deployment-8557b8bcb-5nxtw:/$ kubectl get pods --v=8
I1226 18:34:53.023563 117 merged_client_builder.go:163] Using in-cluster namespace
I1226 18:34:53.023718 117 merged_client_builder.go:121] Using in-cluster configuration
I1226 18:34:53.026928 117 merged_client_builder.go:121] Using in-cluster configuration
I1226 18:34:53.027091 117 round_trippers.go:463] GET https://10.96.0.1:443/api/v1/namespaces/default/pods?limit=500
I1226 18:34:53.027104 117 round_trippers.go:469] Request Headers:
I1226 18:34:53.027108 117 round_trippers.go:473] Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json
I1226 18:34:53.027112 117 round_trippers.go:473] User-Agent: kubectl/v1.29.0 (linux/arm64) kubernetes/3f7a50f
I1226 18:34:53.027125 117 round_trippers.go:473] Authorization: Bearer <masked>
I1226 18:34:53.031586 117 round_trippers.go:574] Response Status: 200 OK in 4 milliseconds
I1226 18:34:53.031603 117 round_trippers.go:577] Response Headers:
I1226 18:34:53.031609 117 round_trippers.go:580] Audit-Id: e6632157-089c-4d76-b785-5d5cf17ce21d
I1226 18:34:53.031613 117 round_trippers.go:580] Cache-Control: no-cache, private
I1226 18:34:53.031616 117 round_trippers.go:580] Content-Type: application/json
I1226 18:34:53.031619 117 round_trippers.go:580] X-Kubernetes-Pf-Flowschema-Uid: e89a4367-5d31-4ef4-8ce4-0ab965b47216
I1226 18:34:53.031623 117 round_trippers.go:580] X-Kubernetes-Pf-Prioritylevel-Uid: 00dde4db-4ddd-4725-8f4f-7a5187672b4a
I1226 18:34:53.031626 117 round_trippers.go:580] Date: Tue, 26 Dec 2023 18:34:53 GMT
I1226 18:34:53.031719 117 request.go:1212] Response Body: {"kind":"Table","apiVersion":"meta.k8s.io/v1","metadata":{"resourceVersion":"73373"},"columnDefinitions":[{"name":"Name","type":"string","format":"name","description":"Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names","priority":0},{"name":"Ready","type":"string","format":"","description":"The aggregate readiness state of this pod for accepting traffic.","priority":0},{"name":"Status","type":"string","format":"","description":"The aggregate status of the containers in this pod.","priority":0},{"name":"Restarts","type":"string","format":"","description":"The number of times the containers in this pod have been restarted and when the last container in this pod has restarted.","priority":0},{" [truncated 4077 chars]
NAME READY STATUS RESTARTS AGE
kubectl-deployment-8557b8bcb-5nxtw 1/1 Running 0 14m
And here is kubectl
failing miserably with a --timeout
client-side config:
I have no name!@kubectl-deployment-8557b8bcb-5nxtw:/$ kubectl get pods --request-timeout 30 --v=8
I1226 18:35:14.592315 128 merged_client_builder.go:163] Using in-cluster namespace
I1226 18:35:14.592601 128 round_trippers.go:463] GET http://localhost:8080/api?timeout=30s
I1226 18:35:14.592612 128 round_trippers.go:469] Request Headers:
I1226 18:35:14.592618 128 round_trippers.go:473] Accept: application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json
I1226 18:35:14.592622 128 round_trippers.go:473] User-Agent: kubectl/v1.29.0 (linux/arm64) kubernetes/3f7a50f
I1226 18:35:14.593293 128 round_trippers.go:574] Response Status: in 0 milliseconds
I1226 18:35:14.593335 128 round_trippers.go:577] Response Headers:
E1226 18:35:14.593422 128 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=30s": dial tcp [::1]:8080: connect: connection refused
I1226 18:35:14.593462 128 cached_discovery.go:120] skipped caching discovery info due to Get "http://localhost:8080/api?timeout=30s": dial tcp [::1]:8080: connect: connection refused
Notice anything different?
Yep! This line GET http://localhost:8080/api?timeout=30s
is very very wrong for an in-cluster setup. It should be:
https://10.96.0.1:443/api/v1/namespaces/default/pods?limit=500timeout=30s
So what’s going on?
Currently kubectl –request-timeout (or any other client configuration flag) does not work with in-cluster configuration because isDefaultConfig returns false when checking for equality on this line and then the mergedConfig is used instead of the correct in cluster config.
TL;DR Merging was correctly handled in the in-cluster case so client side config (–timeout, –as) wasn’t being correctly applied.
Another neat kubectl
trick I learned from debugging- there’s a --raw
flag you can use to directly send the request!
❯ kubectl get --raw="https://127.0.0.1:51532/api/v1/pods?limit=1"
{"kind":"PodList","apiVersion":"v1","metadata" .... lots of random pod info }
I usually don’t run kubectl from an in-cluster pod, so it was fun figuring out the best way to reproduce the issue. First roadblock- my work laptop is a very fancy arm Mac, so I needed to figure out how to cross compile kubectl
. Digging into the makefile, I eventually figured out the right magic encantation and we were cooking:
build/run.sh make kubectl KUBE_BUILD_PLATFORMS=linux/amd64
Next- I needed some image with my custom build kubectl
to make sure the fix actually worked. Easy, peasy, container-eezy, just needed a small dockerfile:
FROM ubuntu:20.04
WORKDIR /app
COPY kubectl /usr/local/bin/
RUN chmod +x /usr/local/bin/kubectl
USER 1001
ENTRYPOINT [ "kubectl" ]
CMD [ "--help" ]
This docker copies the kubectl
I built above that get spit out at kubernetes/_output/dockerized/bin/linux/amd64
and puts it in the new container’s /usr/local/bin/
and then makes it execuctable. Building this dockerfile is super quick because it has nothing in other than the base ubuntu:20.04 and kubectl. I just ran this where I defined the file:
docker build -t nina-kubectl -f test.Dockerfile .
Next, I needed a basic kind cluster. It didn’t need to be super fancy (no metallb, single node, no special cluster config changes). Basically kind create cluster
just did the trick.
I created a basic Deployment that used my new docker image:
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubectl-deployment
spec:
replicas: 1
selector:
matchLabels:
app: kubectl
template:
metadata:
labels:
app: kubectl
spec:
serviceAccountName: my-service-account
containers:
- name: kubectl-container
image: nina-kubectl:latest
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-service-account
I execed into the pod that got spun up from my deployment, checked kubectl
was there, and then… I realized I needed some RBAC permissions to actually list the pods… Ughhhh….
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-reader
rules:
- apiGroups: [""]
resources: ["*"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: read-pods
subjects:
- kind: ServiceAccount
name: my-service-account
namespace: default
roleRef:
kind: ClusterRole
name: pod-reader
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: ServiceAccount
name: my-service-account
namespace: default
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
So in order to validate the fix actually worked, I execed into pod and test kubectl behavior with and without client config with fix:
I have no name!@kubectl-deployment-6874f6db87-9s9qb:/app$ kubectl get pods
NAME READY STATUS RESTARTS AGE
kubectl-deployment-6874f6db87-9s9qb 1/1 Running 0 15s
kubectl-deployment-6874f6db87-mfskr 1/1 Terminating 0 61s
pod-identity-webhook-8565f74769-2772d 1/1 Running 0 27h
I have no name!@kubectl-deployment-6874f6db87-9s9qb:/app$ kubectl get pods --request-timeout 30
NAME READY STATUS RESTARTS AGE
kubectl-deployment-6874f6db87-9s9qb 1/1 Running 0 35s
pod-identity-webhook-8565f74769-2772d 1/1 Running 0 27h
Nice! So I wrote up some unit tests, updated an e2e test with client config and put my PR.
I thought I was done…
Alas…
Before you can run the e2e test locally, you need a couple things:
- Gingko v2 which you can grab with
go install github.com/onsi/^Cnkgo/v2/ginkgo@latest
- Build the e2e test frame work with:
make all WHAT=test/e2e/e2e.test vendor/github.com/onsi/ginkgo/v2/ginkgo
- Make sure you have a kind cluster running (Nothing to fancy,
kind create cluster
will work)
Now in order to run the kubectl e2e tests, you need to dig up this line from the test logs:
KUBERNETES_PROVIDER=skeleton ./hack/ginkgo-e2e.sh --ginkgo.focus="should handle in-cluster config" --ginkgo.skip='\[Serial\]|\[Deprecated\]'
But for all my cross-compiling magic, it turned out the e2e kubectl tests don’t run on arm based on this comment I should have read earlier…
// TODO: Find a way to download and copy the appropriate kubectl binary, or maybe a multi-arch kubectl image
// for now this only works on amd64
e2eskipper.SkipUnlessNodeOSArchIs("amd64")
Luckily they worked just fine on my very ancient undergrad ubuntu 18.04 laptop.
But… when I ran they e2e test disaster struck again! The e2e tests use the local kubectl set in your env as the kubectl in the tests:
I0226 14:27:02.844380 462679 builder.go:146] stderr: ""
I0226 14:27:02.844416 462679 builder.go:147] stdout: ""
I0226 14:27:02.844558 462679 kubectl.go:645] copying configmap manifests to the httpd pod
I0226 14:27:02.844614 462679 builder.go:121] Running '/usr/local/bin/kubectl --server=https://127.0.0.1:32947 --kubeconfig=/home/npolshak/.kube/config --namespace=kubectl-4371 cp /tmp/icc-override3336876710/invalid-configmap-with-namespace.yaml kubectl-4371/httpd:/tmp/'
Add to the PATH:
export PATH="/home/npolshak/kubernetes/_output/dockerized/bin/linux/amd64:$PATH"
To be continued…