first commit
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
type Image struct {
|
||||
Name string
|
||||
Pinned bool
|
||||
}
|
||||
|
||||
// ContainerRuntime defines the interface for managing containerd images
|
||||
type ContainerRuntime interface {
|
||||
List() ([]Image, error)
|
||||
Pin(imageRef string) error
|
||||
Unpin(imageRef string) error
|
||||
Pull(imageRef string) error
|
||||
Remove(imageRef string) error
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/leases"
|
||||
"github.com/containerd/containerd/namespaces"
|
||||
)
|
||||
|
||||
// Containerd implements ContainerRuntime interface
|
||||
type Containerd struct {
|
||||
client *containerd.Client
|
||||
ctx context.Context
|
||||
namespace string
|
||||
}
|
||||
|
||||
// NewImageManager creates a new Containerd
|
||||
func NewImageManager(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() ([]Image, 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)
|
||||
}
|
||||
|
||||
// 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 []Image
|
||||
for _, img := range images {
|
||||
result = append(result, Image{
|
||||
Name: img.Name,
|
||||
Pinned: 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-%d", generateID(), time.Now().UnixNano())
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// List all leases
|
||||
leaseList, err := m.client.LeasesService().List(m.ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list leases: %w", err)
|
||||
}
|
||||
|
||||
// Find leases that reference our image
|
||||
var foundLease bool
|
||||
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 {
|
||||
// Delete this lease
|
||||
if err := m.client.LeasesService().Delete(m.ctx, lease); err != nil {
|
||||
return fmt.Errorf("failed to delete lease %s: %w", lease.ID, err)
|
||||
}
|
||||
foundLease = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundLease {
|
||||
return fmt.Errorf("no lease found for image %s", imageRef)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pull pulls an image from a registry
|
||||
func (m *Containerd) Pull(imageRef string) error {
|
||||
// Set up pull options
|
||||
pullOpts := []containerd.RemoteOpt{
|
||||
containerd.WithPlatform(""),
|
||||
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() string {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
// Fall back to a timestamp-based ID in the unlikely event of rand failure
|
||||
return strings.Replace(fmt.Sprintf("%d", time.Now().UnixNano()), "-", "", -1)
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Default containerd socket path
|
||||
socketPath := "/run/containerd/containerd.sock"
|
||||
// Default namespace for Docker with containerd
|
||||
namespace := "moby"
|
||||
|
||||
// Create the image manager
|
||||
imgManager, err := NewImageManager(socketPath, namespace)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create image manager: %v", err)
|
||||
}
|
||||
|
||||
img := "docker.io/library/alpine:3.20.3"
|
||||
err = imgManager.Pull(img)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
imgs, err := imgManager.List()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Images: %v\n", imgs)
|
||||
|
||||
err = imgManager.Pin(img)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
imgs, err = imgManager.List()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Images: %v\n", imgs)
|
||||
|
||||
err = imgManager.Unpin(img)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
imgs, err = imgManager.List()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Images: %v\n", imgs)
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user