Initial pull implemented.
Support for proxy repositories by pulling from the proxy and then tagging the image.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user