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 }