Initial pull implemented.

Support for proxy repositories by pulling from the proxy and then tagging the image.
This commit is contained in:
2025-03-09 14:18:29 +01:00
parent 965dfbf245
commit b31e03bc7a
13 changed files with 292 additions and 15 deletions
+2
View File
@@ -3,6 +3,7 @@ package main
import "time"
type Config struct {
InitialPullAll bool
PollInterval time.Duration
KubernetesNamespace string
SocketPath string
@@ -11,4 +12,5 @@ type Config struct {
ReadyDuration time.Duration
includeControllerNodes bool
monitoringWindowSize time.Duration
mirrors map[string]string
}
-11
View File
@@ -1,11 +0,0 @@
package main
// ContainerRuntime defines the interface for managing containerd images
type ContainerRuntime interface {
// List images: returns map of container image to whether or not it is pinned
List() (map[string]bool, error)
Pin(imageRef string) error
Unpin(imageRef string) error
Pull(imageRef string) error
Remove(imageRef string) error
}
-193
View File
@@ -1,193 +0,0 @@
package main
import (
"context"
"crypto/md5"
"encoding/base64"
"fmt"
"github.com/containerd/containerd"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/leases"
"github.com/containerd/containerd/namespaces"
"k8s.io/klog/v2"
)
// Containerd implements ContainerRuntime interface
type Containerd struct {
client *containerd.Client
ctx context.Context
namespace string
}
// NewContainerd creates a new Containerd
func NewContainerd(socketPath, namespace string) (*Containerd, error) {
client, err := containerd.New(socketPath)
if err != nil {
return nil, fmt.Errorf("failed to connect to containerd at %s: %w", socketPath, err)
}
client.LeasesService()
ctx := namespaces.WithNamespace(context.Background(), namespace)
return &Containerd{
client: client,
ctx: ctx,
namespace: namespace,
}, nil
}
// List returns all images with their pinned status
func (m *Containerd) List() (map[string]bool, error) {
// Get all images
images, err := m.client.ImageService().List(m.ctx)
if err != nil {
return nil, fmt.Errorf("failed to list images: %w", err)
}
for _, image := range images {
klog.V(3).Infof("Image '%s' digest '%s'", image.Name,
image.Target.Digest.String())
}
// Get all leases
leases, err := m.client.LeasesService().List(m.ctx)
if err != nil {
return nil, fmt.Errorf("failed to list leases: %w", err)
}
// Create a map of image references that are pinned
pinnedImages := make(map[string]bool)
for _, lease := range leases {
// Check if lease has labels referencing an image
if label, ok := lease.Labels["containerd.io/gc.ref.content.image"]; ok {
pinnedImages[label] = true
}
}
// Create the result list
var result = make(map[string]bool)
for _, img := range images {
result[img.Name] = pinnedImages[img.Name]
}
return result, nil
}
// Pin creates a lease for an image to prevent garbage collection
func (m *Containerd) Pin(imageRef string) error {
// Create a unique lease ID based on image reference
leaseID := fmt.Sprintf("pin-%s", generateID(imageRef))
// Get the image to validate it exists
_, err := m.client.ImageService().Get(m.ctx, imageRef)
if err != nil {
return fmt.Errorf("failed to get image %s: %w", imageRef, err)
}
leaseList, err := m.findLeases(imageRef)
if err != nil {
return fmt.Errorf("Failed to get leases for image %s: %v", imageRef, err)
}
if len(leaseList) > 0 {
return nil
}
// Create a new lease
opts := []leases.Opt{
leases.WithID(leaseID),
leases.WithLabels(map[string]string{
"containerd.io/gc.ref.content.image": imageRef,
}),
}
_, err = m.client.LeasesService().Create(m.ctx, opts...)
if err != nil {
return fmt.Errorf("failed to create lease: %w", err)
}
return nil
}
// Unpin removes a lease for an image allowing garbage collection
func (m *Containerd) Unpin(imageRef string) error {
leases, err := m.findLeases(imageRef)
if err != nil {
return err
}
for _, lease := range leases {
if err := m.client.LeasesService().Delete(m.ctx, lease); err != nil {
return fmt.Errorf("failed to delete lease %s: %w", lease.ID, err)
}
}
return nil
}
func (m *Containerd) findLeases(imageRef string) ([]leases.Lease, error) {
// List all leases
leaseList, err := m.client.LeasesService().List(m.ctx)
if err != nil {
return nil, fmt.Errorf("failed to list leases: %w", err)
}
// Find leases that reference our image
var leases = make([]leases.Lease, 0)
for _, lease := range leaseList {
// Check if this lease has a label referencing our image
if label, ok := lease.Labels["containerd.io/gc.ref.content.image"]; ok && label == imageRef {
leases = append(leases, lease)
}
}
return leases, nil
}
// Pull pulls an image from a registry
func (m *Containerd) Pull(imageRef string) error {
// Set up pull options
pullOpts := []containerd.RemoteOpt{
//containerd.WithPlatformMatcher(platforms.Default()),
containerd.WithPullUnpack,
}
// Pull the image
_, err := m.client.Pull(m.ctx, imageRef, pullOpts...)
if err != nil {
return fmt.Errorf("failed to pull image %s: %w", imageRef, err)
}
return nil
}
// Remove deletes an image
func (m *Containerd) Remove(imageRef string) error {
// Get the image
_, err := m.client.ImageService().Get(m.ctx, imageRef)
if err != nil {
if errdefs.IsNotFound(err) {
return fmt.Errorf("image %s not found", imageRef)
}
return fmt.Errorf("failed to get image %s: %w", imageRef, err)
}
// Delete the image
err = m.client.ImageService().Delete(m.ctx, imageRef, images.SynchronousDelete())
if err != nil {
return fmt.Errorf("failed to delete image %s: %w", imageRef, err)
}
return nil
}
// Close closes the containerd client connection
func (m *Containerd) Close() error {
return m.client.Close()
}
// generateID creates a random unique ID
func generateID(image string) string {
md5Hash := md5.Sum([]byte(image))
md5Base64 := base64.StdEncoding.EncodeToString(md5Hash[:])
return md5Base64
}
+18 -3
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
containerd2 "git.wamblee.org/public/kube-fetcher/pkg/ctrd"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
@@ -108,7 +109,7 @@ func (fetcher *Fetcher) getContainers(clientset *kubernetes.Clientset) map[strin
return containers
}
func (fetcher *Fetcher) pullAndPin() error {
func (fetcher *Fetcher) pullAndPin(pullAll bool) error {
nodeName := os.Getenv("NODE_NAME")
if nodeName == "" {
@@ -116,7 +117,7 @@ func (fetcher *Fetcher) pullAndPin() error {
}
// Create the image manager
containerd, err := NewContainerd(fetcher.config.SocketPath, fetcher.config.ContainerdNamespace)
containerd, err := containerd2.NewContainerd(fetcher.config.SocketPath, fetcher.config.ContainerdNamespace)
if err != nil {
klog.Fatalf("Failed to create image manager: %v", err)
}
@@ -144,12 +145,26 @@ func (fetcher *Fetcher) pullAndPin() error {
// Pull images that are used
for container := range containers {
if _, found := imgs[container]; !found {
if _, found := imgs[container]; !found || pullAll {
tag := ""
for registry, mirror := range fetcher.config.mirrors {
if strings.HasPrefix(container, registry+"/") {
tag = container
container = mirror + container[len(registry):]
}
}
klog.Infof("%s: Pulling %s\n", nodeName, container)
err := containerd.Pull(container)
if err != nil {
klog.Warningf("error: %v", err)
}
if tag != "" {
klog.Infof("%s: Tagging '%s' -> '%s'", nodeName, container, tag)
err := containerd.Tag(container, tag)
if err != nil {
klog.Warningf("Could not tag '%s' -> '%s'", container, tag)
}
}
}
}
-25
View File
@@ -1,25 +0,0 @@
package main
import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"log"
)
func GetKubernetesConnection() *kubernetes.Clientset {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
config, err := kubeConfig.ClientConfig()
if err != nil {
log.Panicln(err.Error())
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
log.Panicln(err.Error())
}
return clientset
}
+10 -2
View File
@@ -2,6 +2,7 @@ package main
import (
goflags "flag"
"git.wamblee.org/public/kube-fetcher/pkg/support"
"github.com/spf13/cobra"
"k8s.io/klog/v2"
"os"
@@ -12,7 +13,7 @@ func main() {
klogFlags := goflags.NewFlagSet("", goflags.PanicOnError)
klog.InitFlags(klogFlags)
clientset := GetKubernetesConnection()
clientset := support.GetKubernetesConnection()
config := &Config{}
cmd := &cobra.Command{
@@ -23,6 +24,7 @@ Queries k8s for all running pods and makes sure that all
images referenced in pods are made available on the local k8s node and pinned
so they don't get garbage collected'`,
RunE: func(cmd *cobra.Command, args []string) error {
serializer := make(chan func())
go func() {
for action := range serializer {
@@ -32,13 +34,15 @@ so they don't get garbage collected'`,
watcher := NewWatcher(clientset, config.monitoringWindowSize, config.KubernetesNamespace, serializer)
fetcher := NewFetcher(clientset, config, watcher)
// TODO config option
fetcher.pullAndPin(config.InitialPullAll)
ticker := time.NewTicker(config.PollInterval)
for {
select {
case <-ticker.C:
serializer <- func() {
klog.V(3).Infof("Fetcher.pullAndPin")
fetcher.pullAndPin()
fetcher.pullAndPin(false)
}
}
}
@@ -61,6 +65,10 @@ so they don't get garbage collected'`,
6*time.Hour, "Monitoring window to see what pods were active")
cmd.PersistentFlags().DurationVar(&config.PollInterval, "poll-interval",
1*time.Minute, "Poll interval for checking whether to pull images. ")
cmd.PersistentFlags().StringToStringVar(&config.mirrors,
"mirror", make(map[string]string), "Specify regsitry mirror in the form registrey=mirror, e.g. docker.io=my.mirror. The option can be repeated.")
cmd.PersistentFlags().BoolVar(&config.InitialPullAll, "initial-pull-all",
false, "Initially pull all images, this can be usefule for populating a caching proxy.")
cmd.Flags().AddGoFlagSet(klogFlags)
err := cmd.Execute()