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 }