kube-fetcher/cmd/fetcher/fetcher.go
Erik Brakkee e8d4adaf53 periodically checking now for pods.
Now also dealing with deleted resources in the time interval.
Making sure that new pods are not immediately synced on other nodes to avoid pull rate limits caused by this code.
2025-03-02 19:47:44 +01:00

214 lines
5.8 KiB
Go

package main
import (
"context"
"fmt"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
"os"
"regexp"
"strings"
"time"
)
type Fetcher struct {
clientset *kubernetes.Clientset
config *Config
watcher *Watcher
}
func NewFetcher(clientset *kubernetes.Clientset, config *Config, watcher *Watcher) *Fetcher {
return &Fetcher{
config: config,
clientset: clientset,
watcher: watcher,
}
}
func (fetcher *Fetcher) canonicalizeImageName(image string) string {
pattern := `^(?:(?P<registry>[a-zA-Z0-9][-a-zA-Z0-9.]*[a-zA-Z0-9](?::[0-9]+)?)/)?(?P<repository>(?:[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)?)?(?::(?P<tag>[\w][\w.-]{0,127}))?(?:@(?P<digest>[a-z][a-z0-9]*(?:[+.-][a-z][a-z0-9]*)*:[a-zA-Z0-9*+,-./:;=@_]{32,}))?$`
re := regexp.MustCompile(pattern)
matches := re.FindStringSubmatch(image)
subexpNames := re.SubexpNames()
if matches == nil {
panic(fmt.Errorf("Invalid image reference: %s\n", image))
}
result := make(map[string]string)
for i, name := range subexpNames {
if i != 0 && name != "" && i < len(matches) {
result[name] = matches[i]
}
}
klog.V(3).Infof("Image: %s\n", image)
klog.V(3).Infof(" Registry: %s\n", result["registry"])
klog.V(3).Infof(" Repository: %s\n", result["repository"])
klog.V(3).Infof(" Tag: %s\n", result["tag"])
klog.V(3).Infof(" Digest: %s\n", result["digest"])
registry := result["registry"]
repository := result["repository"]
tag := result["tag"]
digest := result["digest"]
// Check if image has a tag
if digest == "" && tag == "" {
tag = "latest"
}
// Check if image has a host
if registry == "" {
registry = "docker.io"
}
// Handle the case when remainder doesn't specify library but it's not a docker.io official image
if registry == "docker.io" && !strings.Contains(repository, "/") {
repository = "library/" + repository
}
fullimage := registry + "/" + repository
if tag != "" {
fullimage += ":" + tag
}
if digest != "" {
fullimage += "@" + digest
}
return fullimage
}
func (fetcher *Fetcher) wasReady(pod *v1.Pod, controllers map[string]bool) bool {
if !fetcher.config.includeControllerNodes {
klog.V(3).Infof("Checking %s (%s)", pod.Name, pod.Spec.NodeName)
if _, ok := controllers[pod.Spec.NodeName]; ok {
return false
}
}
return time.Now().Sub(pod.CreationTimestamp.Time) >= fetcher.config.ReadyDuration
}
func (fetcher *Fetcher) getContainers(clientset *kubernetes.Clientset) map[string]bool {
controllers := fetcher.getControllerNames(clientset)
pods := fetcher.watcher.getPods()
//pods, err := clientset.CoreV1().Pods(fetcher.config.KubernetesNamespace).List(context.Background(),
// metav1.ListOptions{})
//if err != nil {
// panic(err)
//}
containers := make(map[string]bool)
containersOnCurrentNode := make(map[string]bool)
for _, pod := range pods {
klog.V(3).Infof("%s/%s\n", pod.Namespace, pod.Name)
for _, container := range pod.Spec.InitContainers {
klog.V(3).Infof(" %s\n", container.Image)
if pod.Spec.NodeName == fetcher.config.Nodename {
containersOnCurrentNode[fetcher.canonicalizeImageName(container.Image)] = true
} else {
if fetcher.wasReady(pod, controllers) {
containers[fetcher.canonicalizeImageName(container.Image)] = true
}
}
}
for _, container := range pod.Spec.Containers {
klog.V(3).Infof(" %s\n", container.Image)
if pod.Spec.NodeName == fetcher.config.Nodename {
containersOnCurrentNode[fetcher.canonicalizeImageName(container.Image)] = true
} else {
if fetcher.wasReady(pod, controllers) {
containers[fetcher.canonicalizeImageName(container.Image)] = true
}
}
}
}
for container := range containersOnCurrentNode {
delete(containers, container)
}
return containers
}
func (fetcher *Fetcher) pullAndPin() error {
// Create the image manager
containerd, err := NewContainerd(fetcher.config.SocketPath, fetcher.config.ContainerdNamespace)
if err != nil {
klog.Fatalf("Failed to create image manager: %v", err)
}
imgs, err := containerd.List()
if err != nil {
return err
}
containers := fetcher.getContainers(fetcher.clientset)
for container := range containers {
klog.V(3).Infof("Found container %s\n", container)
}
// unpin images that are not used
for container, pinned := range imgs {
if !containers[container] && pinned {
klog.Infof("Unpinning %s\n", container)
err := containerd.Unpin(container)
if err != nil {
klog.Warningf(" error: %v", err)
}
}
}
// Pull images that are used
for container := range containers {
if _, found := imgs[container]; !found {
klog.Infof("Pulling %s\n", container)
err := containerd.Pull(container)
if err != nil {
klog.Warningf("error: %v", err)
}
}
}
imgs, err = containerd.List()
if err != nil {
return err
}
// Pin images that are used and present
for container := range containers {
if pinned, found := imgs[container]; found && !pinned {
klog.Infof("Pinning %s\n", container)
err := containerd.Pin(container)
if err != nil {
klog.Warningf(" error: %v", err)
}
}
}
return nil
}
func (fetcher *Fetcher) getControllerNames(clientset *kubernetes.Clientset) map[string]bool {
nodes, err := clientset.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{
LabelSelector: "node-role.kubernetes.io/control-plane=,!node-role.kubernetes.io/master",
})
if err != nil {
fmt.Printf("Error listing nodes: %v\n", err)
os.Exit(1)
}
nodeset := make(map[string]bool)
for _, node := range nodes.Items {
nodeset[node.Name] = true
}
return nodeset
}
// TODO
// 1. periodic pull and pin, configurable time interval
// 2. logging summary results of each pull and pin
// 3. docker container with multi-stage build
// 4. kustomize deployment in wamb lee-operators namespace (kubernetes-setup repo)