github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/docker/volume.go (about) 1 package docker 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "runtime" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/blang/semver/v4" 17 "github.com/docker/docker/api/types" 18 "github.com/docker/docker/api/types/volume" 19 dockerClient "github.com/docker/docker/client" 20 21 "github.com/datawire/dlib/dlog" 22 "github.com/telepresenceio/telepresence/v2/pkg/client" 23 "github.com/telepresenceio/telepresence/v2/pkg/client/cache" 24 "github.com/telepresenceio/telepresence/v2/pkg/proc" 25 ) 26 27 // EnsureVolumePlugin checks if the datawire/telemount plugin is installed and installs it if that is 28 // not the case. The plugin is also enabled. 29 func EnsureVolumePlugin(ctx context.Context) (string, error) { 30 cli, err := GetClient(ctx) 31 if err != nil { 32 return "", err 33 } 34 cfg := client.GetConfig(ctx).Intercept().Telemount 35 pn := pluginName(ctx) 36 if pt := cfg.Tag; pt != "" { 37 pn += "-" + pt 38 } else if lv, err := latestPluginVersion(ctx, pn); err == nil { 39 pn += "-" + lv.String() 40 } else { 41 dlog.Warnf(ctx, "failed to get latest version of docker volume plugin %s: %v", pn, err) 42 } 43 pi, _, err := cli.PluginInspectWithRaw(ctx, pn) 44 if err != nil { 45 if !dockerClient.IsErrNotFound(err) { 46 dlog.Errorf(ctx, "docker plugin inspect: %v", err) 47 } 48 return pn, installVolumePlugin(ctx, pn) 49 } 50 if !pi.Enabled { 51 err = cli.PluginEnable(ctx, pn, types.PluginEnableOptions{Timeout: 5}) 52 } 53 return pn, err 54 } 55 56 func pluginName(ctx context.Context) string { 57 tm := client.GetConfig(ctx).Intercept().Telemount 58 return fmt.Sprintf("%s/%s/%s:%s", tm.Registry, tm.Namespace, tm.Repository, runtime.GOARCH) 59 } 60 61 func installVolumePlugin(ctx context.Context, pluginName string) error { 62 dlog.Debugf(ctx, "Installing docker volume plugin %s", pluginName) 63 cmd := proc.CommandContext(ctx, "docker", "plugin", "install", "--grant-all-permissions", pluginName) 64 _, err := proc.CaptureErr(cmd) 65 if err != nil { 66 err = fmt.Errorf("docker plugin install %s: %w", pluginName, err) 67 } 68 return err 69 } 70 71 type pluginInfo struct { 72 LatestVersion string `json:"latestVersions"` 73 LastCheck int64 `json:"lastCheck"` 74 } 75 76 const pluginInfoMaxAge = 24 * time.Hour 77 78 func latestPluginVersion(ctx context.Context, pluginName string) (ver semver.Version, err error) { 79 file := "volume-plugin-info.json" 80 pi := pluginInfo{} 81 if err = cache.LoadFromUserCache(ctx, &pi, file); err != nil { 82 if !os.IsNotExist(err) { 83 return ver, err 84 } 85 pi.LastCheck = 0 86 } 87 88 now := time.Now().UnixNano() 89 if time.Duration(now-pi.LastCheck) > pluginInfoMaxAge { 90 ver, err = getLatestPluginVersion(ctx, pluginName) 91 if err == nil { 92 pi.LatestVersion = ver.String() 93 pi.LastCheck = now 94 err = cache.SaveToUserCache(ctx, &pi, file, cache.Public) 95 } 96 } else { 97 dlog.Debugf(ctx, "Using cached version %s for %s", pi.LatestVersion, pluginName) 98 ver, err = semver.Parse(pi.LatestVersion) 99 } 100 return ver, err 101 } 102 103 type imgResult struct { 104 Name string `json:"name"` 105 } 106 type repsResponse struct { 107 Results []imgResult `json:"results"` 108 } 109 110 func getLatestPluginVersion(ctx context.Context, pluginName string) (ver semver.Version, err error) { 111 dlog.Debugf(ctx, "Checking for latest version of %s", pluginName) 112 cfg := client.GetConfig(ctx).Intercept().Telemount 113 url := fmt.Sprintf("https://%s/namespaces/%s/repositories/%s/tags", cfg.RegistryAPI, cfg.Namespace, cfg.Repository) 114 var rq *http.Request 115 rq, err = http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 116 if err != nil { 117 return ver, err 118 } 119 rq.Header.Add("Accept", "application/json") 120 var rs *http.Response 121 rs, err = http.DefaultClient.Do(rq) 122 if err != nil { 123 return ver, err 124 } 125 var data []byte 126 data, err = io.ReadAll(rs.Body) 127 if err != nil { 128 return ver, err 129 } 130 _ = rs.Body.Close() 131 if rs.StatusCode != http.StatusOK { 132 return ver, errors.New(rs.Status) 133 } 134 var infos repsResponse 135 err = json.Unmarshal(data, &infos) 136 if err != nil { 137 return ver, err 138 } 139 pfx := runtime.GOARCH + "-" 140 for _, info := range infos.Results { 141 if strings.HasPrefix(info.Name, pfx) { 142 iv, err := semver.Parse(strings.TrimPrefix(info.Name, pfx)) 143 if err == nil && iv.GT(ver) { 144 ver = iv 145 } 146 } 147 } 148 dlog.Debugf(ctx, "Found latest version of %s to be %s", pluginName, ver) 149 return ver, err 150 } 151 152 func StartVolumeMounts(ctx context.Context, pluginName, dcName, container string, sftpPort int32, mounts, vols []string) ([]string, error) { 153 host, err := ContainerIP(ctx, dcName) 154 if err != nil { 155 return nil, fmt.Errorf("failed to retrieved container ip for %s: %w", dcName, err) 156 } 157 for i, dir := range mounts { 158 v := fmt.Sprintf("%s-%d", container, i) 159 if err := startVolumeMount(ctx, pluginName, host, sftpPort, v, container, dir); err != nil { 160 return vols, err 161 } 162 vols = append(vols, v) 163 } 164 return vols, nil 165 } 166 167 func StopVolumeMounts(ctx context.Context, vols []string) { 168 for _, vol := range vols { 169 if err := stopVolumeMount(ctx, vol); err != nil { 170 dlog.Error(ctx, err) 171 } 172 } 173 } 174 175 func startVolumeMount(ctx context.Context, pluginName, host string, port int32, volumeName, container, dir string) error { 176 cli, err := GetClient(ctx) 177 if err != nil { 178 return err 179 } 180 _, err = cli.VolumeCreate(ctx, volume.CreateOptions{ 181 Driver: pluginName, 182 DriverOpts: map[string]string{ 183 "host": host, 184 "container": container, 185 "port": strconv.Itoa(int(port)), 186 "dir": dir, 187 }, 188 Name: volumeName, 189 }) 190 if err != nil { 191 err = fmt.Errorf("docker volume create %d %s %s: %w", port, container, dir, err) 192 } 193 return err 194 } 195 196 func stopVolumeMount(ctx context.Context, volume string) error { 197 cli, err := GetClient(ctx) 198 if err != nil { 199 return err 200 } 201 err = cli.VolumeRemove(ctx, volume, false) 202 if err != nil { 203 err = fmt.Errorf("docker volume rm %s: %w", volume, err) 204 } 205 return err 206 } 207 208 // ContainerIP returns the IP assigned to the container with the given name on the telepresence network. 209 func ContainerIP(ctx context.Context, name string) (string, error) { 210 cli, err := GetClient(ctx) 211 if err != nil { 212 return "", err 213 } 214 ci, err := cli.ContainerInspect(ctx, name) 215 if err != nil { 216 return "", fmt.Errorf("docker container inspect %s: %w", "userd", err) 217 } 218 if ns := ci.NetworkSettings; ns != nil { 219 if tn, ok := ns.Networks["telepresence"]; ok { 220 return tn.IPAddress, nil 221 } 222 } 223 return "", os.ErrNotExist 224 }