kube-fetcher/pkg/ctrd/containerd.go
Erik Brakkee b31e03bc7a Initial pull implemented.
Support for proxy repositories by pulling from the proxy and then tagging the image.
2025-03-09 14:18:29 +01:00

241 lines
6.1 KiB
Go

package ctrd
import (
"context"
"crypto/md5"
"encoding/base64"
"fmt"
"github.com/containerd/containerd"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/leases"
"github.com/containerd/containerd/namespaces"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"io"
"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
}
func (m *Containerd) Tag(imageFrom, imageTo string) error {
sourceImage, err := m.client.GetImage(m.ctx, imageFrom)
if err != nil {
return fmt.Errorf("Failed to get source image: %v", err)
}
// Create a new image with the target name that references the same content
newImage := images.Image{
Name: imageTo,
Target: sourceImage.Target(),
}
// best effort, remove old tag
_ = m.Remove(imageTo)
// Create the new tag
if _, err := m.client.ImageService().Create(m.ctx, newImage); err != nil {
return fmt.Errorf("Failed to create new image tag: %v", 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
}
func (m *Containerd) GetBlob(hash string) (io.Reader, error) {
store := m.client.ContentStore()
descriptor := ocispec.Descriptor{
Digest: digest.Digest(hash),
}
reader, err := store.ReaderAt(m.ctx, descriptor)
if err != nil {
return nil, err
}
return content.NewReader(reader), nil
}
func (m *Containerd) GetBlobMeta(hash string) (content.Info, error) {
store := m.client.ContentStore()
info, err := store.Info(m.ctx, digest.Digest(hash))
if err != nil {
return content.Info{}, err
}
return info, nil
}