Cómo crear un plugin de kubectl con client-go para eliminar un Namespace
Introducción
A medida que Kubernetes continúa dominando la gestión de infraestructuras modernas, herramientas como kubectl se han vuelto esenciales para interactuar con los clústeres de Kubernetes. Sirviendo como una navaja suiza, kubectl permite a los usuarios ejecutar diversas operaciones en sus clústeres. Además, su extensibilidad permite la creación de plugins personalizados, mejorando así su funcionalidad.
En esta publicación, te mostraré cómo crear un plugin kubectl
usando Go y client-go.
El plugin te permitirá eliminar un Namespace de Kubernetes que se ha quedado en estado Terminating
.
Comencemos por comprender el problema que resolverá este plugin para kubectl
.
Namespace bloqueado en estado Terminating
Un Namespace de Kubernetes es un mecanismo para aislar recursos u objetos de Kubernetes dentro de un clúster.
Un Namespace normalmente alojará Deployments, Pods, Services, Ingresses y otros objetos de Kubernetes de tipo Namespaced.
Cuando eliminas un Namespace de Kubernetes, el servidor API lo marcará como Terminating
y comenzará un proceso en segundo plano para eliminar todos los recursos contenidos en el mismo.
Eventualmente, el clúster eliminará el Namespace.
Sin embargo, hay ocasiones en las que el Namespace se queda bloqueado en el estado Terminating
.
Esto puede suceder cuando una extensión de la API de Kubernetes no está disponible y el servidor API no puede eliminar esos recursos.
En este caso, el Namespace contendrá un finalizador que impide que el Namespace se elimine.
El siguiente fragmento de código contiene un Namespace con finalizadores pendientes:
apiVersion: v1
kind: Namespace
metadata:
name: my-namespace
spec:
finalizers:
- kubernetes
status:
conditions:
- lastTransitionTime: "2023-11-16T10:31:38Z"
message: All content-preserving finalizers finished
reason: ContentHasNoFinalizers
status: "False"
type: NamespaceFinalizersRemaining
phase: Terminating
Para poder forzar la eliminación del Namespace, necesitamos eliminar los finalizadores primero.
Esto se puede lograr usando el subrecurso /finalize
de la API de Namespace.
Puedes encontrar información en línea sobre cómo solucionar esto desde la línea de comandos. Mi colega, Jens Reimann, incluso tiene un script muy popular que automatiza los pasos necesarios.
Teniendo en cuenta que este es un problema recurrente, creo que es un buen ejemplo para demostrar cómo crear un plugin kubectl
.
Creando el plugin kubectl
Un plugin kubectl
es, en esencia, un binario con el nombre kubectl-<nombre-del-plugin>
que está disponible en el PATH
del sistema.
Para crear nuestro plugin, necesitaremos un proyecto Go que construya un binario con el nombre kubectl-kill-ns
.
De esta forma, podremos invocar el plugin con kubectl kill ns my-stuck-namespace
.
Comencemos por crear un nuevo módulo Go para nuestro plugin:
mkdir kubectl-kill-ns
cd kubectl-kill-ns
go mod init github.com/marcnuri-demo/kubectl-kill-ns
Si el proceso se ejecuta sin problemas, deberías tener un nuevo archivo go.mod
con el siguiente contenido:
module github.com/marcnuri-demo/kubectl-kill-ns
go 1.21.2
Tendremos que añadir la dependencia k8s.io/client-go
a nuestro proyecto.
Para ello, ejecutaremos el siguiente comando:
go get k8s.io/client-go@v0.28.3
Dado que necesitamos crear un binario con un nombre específico, necesitaremos ejecutar el comando de compilación de Go con el flag -o
para especificar el nombre del archivo de salida.
Para facilitar esto, añadiremos un Makefile
al proyecto con el siguiente contenido:
.PHONY: build
build:
go build -ldflags "-s -w" -o kubectl-kill-ns ./cmd/main.go
Implementando la lógica para eliminar el Namespace
La lógica para eliminar el Namespace la implementaremos en el paquete killns
.
Creemos un nuevo archivo killns.go
en el directorio internal/killns
.
El punto de entrada para client-go es una configuración del clúster de Kubernetes.
Dado que el plugin está destinado a ser utilizado por un usuario que opera desde fuera del clúster, usaremos el siguiente código para detectar y cargar automáticamente la configuración del usuario .kube/config
:
package killns
import (
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)
func LoadKubeConfig() (*rest.Config, error) {
home := homedir.HomeDir()
kubeConfig := filepath.Join(home, ".kube", "config")
return clientcmd.BuildConfigFromFlags("", kubeConfig)
}
El fragmento anterior comienza obteniendo el directorio de inicio del usuario para la plataforma del sistema actual.
A continuación, construye la ruta al archivo .kube/config
del usuario y lo utiliza para cargar la configuración de Kubernetes.
La función devuelve la configuración cargada o un error si no se ha podido cargar la configuración.
La siguiente parte del archivo killns.go
contiene la lógica para eliminar el Namespace.
El siguiente fragmento de código contiene las partes relevantes de la función KillNamespace
:
func KillNamespace(kubeConfig *rest.Config, namespace string) {
//... config validations
client, err := kubernetes.NewForConfig(kubeConfig)
//... client validation
if errDelete := client.CoreV1().Namespaces().Delete(context.TODO(), namespace, metav1.DeleteOptions{}); errDelete != nil {
panic(errDelete)
}
ns, errGet := client.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{})
//... Namespace and finalizer checks
ns.Spec.Finalizers = []corev1.FinalizerName{}
_, errUpdate := client.CoreV1().Namespaces().Finalize(context.TODO(), ns, metav1.UpdateOptions{})
//...
}
La lógica de la función es bastante sencilla:
- Creamos un cliente de Kubernetes usando la configuración proporcionada.
- A continuación, intentamos eliminar el Namespace.
- Si el Namespace sigue existiendo después de la eliminación, comprobamos si el Namespace contiene finalizers.
- Por último, si el Namespace contiene finalizers, los eliminamos y los actualizamos usando el subrecurso
/finalize
.
Implementado el punto de entrada del plugin
El punto de entrada del plugin es el código que se ejecutará cuando se invoque el plugin.
Crearemos un nuevo archivo main.go
en el directorio cmd
con el siguiente contenido:
package main
// imports
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Error:", r)
os.Exit(1)
}
}()
kubeConfig, err := killns.LoadKubeConfig()
if err != nil {
fmt.Println(".kube/config not found", err)
os.Exit(1)
}
flag.Parse()
killns.KillNamespace(kubeConfig, flag.Arg(0))
}
La función es una aplicación de línea de comandos sencilla:
- Comienza diferiendo una función para recuperarse de cualquier panic que mostrará el mensaje de error y terminará la aplicación.
- A continuación, carga la configuración de Kubernetes del archivo
.kube/config
del usuario. - Por último, recupera los argumentos de la línea de comandos e invoca la función
KillNamespace
con la configuración cargada y el nombre del Namespace proporcionado en el primer argumento.
Construyendo el plugin
Emplearemos el Makefile
que creamos anteriormente para construir el plugin.
Para ello, ejecutaremos el siguiente comando:
make build
Un nuevo binario kubectl-kill-ns
debería crearse en el directorio del proyecto.
Probando el plugin
Ahora que tenemos el plugin, podemos probarlo invocándolo directamente desde la línea de comandos:
./kubectl-kill-ns my-stuck-namespace
Considerando que también es un plugin de kubectl
, también podemos probarlo invocándolo mediante kubectl
:
PATH=$PATH:$(pwd) kubectl kill ns my-stuck-namespace
Si queremos que el plugin esté disponible permanentemente, podemos copiarlo a un directorio del PATH
:
Conclusión
En esta publicación, hemos visto cómo crear un plugin kubectl
usando Go y client-go.
El plugin permite eliminar un Namespace de Kubernetes que se ha quedado en estado Terminating
.
Los plugins de kubectl
presentan incontables oportunidades para extender la funcionalidad de Kubernetes de forma sencilla.
Empleando las técnicas descritas en esta publicación, puedes crear soluciones personalizadas para diversos desafíos de Kubernetes.
Puedes encontrar el código fuente completo de este plugin en GitHub.