github.com/benchkram/bob@v0.0.0-20240314204020-b7a57f2f9be9/pkg/dockermobyutil/registry.go (about)

     1  package dockermobyutil
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"math/rand"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/benchkram/errz"
    15  	"github.com/docker/docker/api/types"
    16  	"github.com/docker/docker/client"
    17  )
    18  
    19  var (
    20  	ErrImageNotFound    = fmt.Errorf("image not found")
    21  	ErrConnectionFailed = errors.New("connection to docker daemon failed")
    22  )
    23  
    24  type RegistryClient interface {
    25  	ImageExists(image string) (bool, error)
    26  	ImageHash(image string) (string, error)
    27  	ImageSave(image string) (pathToArchive string, _ error)
    28  	ImageRemove(image string) error
    29  	ImageTag(src string, target string) error
    30  
    31  	ImageLoad(pathToArchive string) error
    32  }
    33  
    34  type R struct {
    35  	client     *client.Client
    36  	archiveDir string
    37  
    38  	// mutex assure to only sequentially access a local docker registry.
    39  	// Some storage driver might not allow for parallel image extraction,
    40  	// @rdnt realised this on his ubuntu22.04 using a zfsfilesystem.
    41  	// Some context https://github.com/moby/moby/issues/21814
    42  	//
    43  	// explicitly using a pointer here to beeing able to detect
    44  	// weather a mutex is required.
    45  	mutex *sync.Mutex
    46  }
    47  
    48  func NewRegistryClient() (RegistryClient, error) {
    49  	cli, err := client.NewClientWithOpts(
    50  		client.FromEnv,
    51  		client.WithAPIVersionNegotiation(),
    52  	)
    53  	if err != nil {
    54  		errz.Fatal(err)
    55  	}
    56  
    57  	r := &R{
    58  		client:     cli,
    59  		archiveDir: os.TempDir(),
    60  	}
    61  
    62  	// Use a lock to suppress parallel image reads on zfs.
    63  	info, err := r.client.Info(context.Background())
    64  	if client.IsErrConnectionFailed(err) {
    65  		return nil, ErrConnectionFailed
    66  	} else if err != nil {
    67  		return nil, err
    68  	}
    69  
    70  	if info.Driver == "zfs" {
    71  		r.mutex = &sync.Mutex{}
    72  	}
    73  
    74  	return r, nil
    75  }
    76  
    77  func (r *R) ImageExists(image string) (bool, error) {
    78  	_, err := r.ImageHash(image)
    79  	if err != nil {
    80  		if errors.Is(err, ErrImageNotFound) {
    81  			return false, nil
    82  		}
    83  		return false, err
    84  	}
    85  
    86  	return true, nil
    87  }
    88  
    89  func (r *R) ImageHash(image string) (string, error) {
    90  	summaries, err := r.client.ImageList(context.Background(), types.ImageListOptions{All: false})
    91  	if err != nil {
    92  		return "", err
    93  	}
    94  
    95  	var selected types.ImageSummary
    96  	for _, s := range summaries {
    97  		for _, rtag := range s.RepoTags {
    98  			if rtag == image {
    99  				selected = s
   100  				break
   101  			}
   102  		}
   103  	}
   104  
   105  	if selected.ID == "" {
   106  		return "", fmt.Errorf("%s, %w", image, ErrImageNotFound)
   107  	}
   108  
   109  	return selected.ID, nil
   110  }
   111  
   112  func (r *R) imageSaveToPath(image string, savedir string) (pathToArchive string, _ error) {
   113  	if r.mutex != nil {
   114  		r.mutex.Lock()
   115  		defer r.mutex.Unlock()
   116  	}
   117  	reader, err := r.client.ImageSave(context.Background(), []string{image})
   118  	if err != nil {
   119  		return "", err
   120  	}
   121  	defer reader.Close()
   122  
   123  	body, err := io.ReadAll(reader)
   124  	if err != nil {
   125  		return "", err
   126  	}
   127  
   128  	// rndExtension is added to the archive name. It prevents overwrite of images in tmp directory in case
   129  	// of a image beeing used as target in multiple tasks (which should be avoided).
   130  	rndExtension := randStringRunes(8)
   131  
   132  	image = strings.ReplaceAll(image, "/", "-")
   133  
   134  	pathToArchive = filepath.Join(savedir, image+"-"+rndExtension+".tar")
   135  	err = os.WriteFile(pathToArchive, body, 0644)
   136  	if err != nil {
   137  		return "", err
   138  	}
   139  
   140  	return pathToArchive, nil
   141  }
   142  
   143  // ImageSave wraps for `docker save` with the addition to add a random string
   144  // to archive name.
   145  func (r *R) ImageSave(image string) (pathToArchive string, _ error) {
   146  	return r.imageSaveToPath(image, r.archiveDir)
   147  }
   148  
   149  // ImageRemove from registry
   150  func (r *R) ImageRemove(imageID string) error {
   151  	options := types.ImageRemoveOptions{
   152  		Force:         true,
   153  		PruneChildren: true,
   154  	}
   155  	_, err := r.client.ImageRemove(context.Background(), imageID, options)
   156  	if err != nil {
   157  		return err
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  // ImageLoad from tar archive
   164  func (r *R) ImageLoad(imgpath string) error {
   165  	f, err := os.Open(imgpath)
   166  	if err != nil {
   167  		return err
   168  	}
   169  	defer f.Close()
   170  
   171  	resp, err := r.client.ImageLoad(context.Background(), f, false)
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	return resp.Body.Close()
   177  }
   178  
   179  // ImageLoad from tar archive
   180  func (r *R) ImageTag(src string, target string) error {
   181  	return r.client.ImageTag(context.Background(), src, target)
   182  }
   183  
   184  // https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
   185  var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz")
   186  
   187  func randStringRunes(n int) string {
   188  	//rand := rand.New(rand.NewSource(time.Now().UnixNano()))
   189  	b := make([]rune, n)
   190  	for i := range b {
   191  		b[i] = letterRunes[rand.Intn(len(letterRunes))]
   192  	}
   193  	return string(b)
   194  }