github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/libpod/plugin/volume_api.go (about) 1 package plugin 2 3 import ( 4 "bytes" 5 "context" 6 "io/ioutil" 7 "net" 8 "net/http" 9 "os" 10 "path/filepath" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/hanks177/podman/v4/libpod/define" 16 "github.com/docker/go-plugins-helpers/sdk" 17 "github.com/docker/go-plugins-helpers/volume" 18 jsoniter "github.com/json-iterator/go" 19 "github.com/pkg/errors" 20 "github.com/sirupsen/logrus" 21 ) 22 23 var json = jsoniter.ConfigCompatibleWithStandardLibrary 24 25 // Copied from docker/go-plugins-helpers/volume/api.go - not exported, so we 26 // need to do this to get at them. 27 // These are well-established paths that should not change unless the plugin API 28 // version changes. 29 var ( 30 activatePath = "/Plugin.Activate" 31 createPath = "/VolumeDriver.Create" 32 getPath = "/VolumeDriver.Get" 33 listPath = "/VolumeDriver.List" 34 removePath = "/VolumeDriver.Remove" 35 hostVirtualPath = "/VolumeDriver.Path" 36 mountPath = "/VolumeDriver.Mount" 37 unmountPath = "/VolumeDriver.Unmount" 38 // nolint 39 capabilitiesPath = "/VolumeDriver.Capabilities" 40 ) 41 42 const ( 43 defaultTimeout = 5 * time.Second 44 volumePluginType = "VolumeDriver" 45 ) 46 47 var ( 48 ErrNotPlugin = errors.New("target does not appear to be a valid plugin") 49 ErrNotVolumePlugin = errors.New("plugin is not a volume plugin") 50 ErrPluginRemoved = errors.New("plugin is no longer available (shut down?)") 51 52 // This stores available, initialized volume plugins. 53 pluginsLock sync.Mutex 54 plugins map[string]*VolumePlugin 55 ) 56 57 // VolumePlugin is a single volume plugin. 58 type VolumePlugin struct { 59 // Name is the name of the volume plugin. This will be used to refer to 60 // it. 61 Name string 62 // SocketPath is the unix socket at which the plugin is accessed. 63 SocketPath string 64 // Client is the HTTP client we use to connect to the plugin. 65 Client *http.Client 66 } 67 68 // This is the response from the activate endpoint of the API. 69 type activateResponse struct { 70 Implements []string 71 } 72 73 // Validate that the given plugin is good to use. 74 // Add it to available plugins if so. 75 func validatePlugin(newPlugin *VolumePlugin) error { 76 // It's a socket. Is it a plugin? 77 // Hit the Activate endpoint to find out if it is, and if so what kind 78 req, err := http.NewRequest("POST", "http://plugin"+activatePath, nil) 79 if err != nil { 80 return errors.Wrapf(err, "error making request to volume plugin %s activation endpoint", newPlugin.Name) 81 } 82 83 req.Header.Set("Host", newPlugin.getURI()) 84 req.Header.Set("Content-Type", sdk.DefaultContentTypeV1_1) 85 86 resp, err := newPlugin.Client.Do(req) 87 if err != nil { 88 return errors.Wrapf(err, "error sending request to plugin %s activation endpoint", newPlugin.Name) 89 } 90 defer resp.Body.Close() 91 92 // Response code MUST be 200. Anything else, we have to assume it's not 93 // a valid plugin. 94 if resp.StatusCode != 200 { 95 return errors.Wrapf(ErrNotPlugin, "got status code %d from activation endpoint for plugin %s", resp.StatusCode, newPlugin.Name) 96 } 97 98 // Read and decode the body so we can tell if this is a volume plugin. 99 respBytes, err := ioutil.ReadAll(resp.Body) 100 if err != nil { 101 return errors.Wrapf(err, "error reading activation response body from plugin %s", newPlugin.Name) 102 } 103 104 respStruct := new(activateResponse) 105 if err := json.Unmarshal(respBytes, respStruct); err != nil { 106 return errors.Wrapf(err, "error unmarshalling plugin %s activation response", newPlugin.Name) 107 } 108 109 foundVolume := false 110 for _, pluginType := range respStruct.Implements { 111 if pluginType == volumePluginType { 112 foundVolume = true 113 break 114 } 115 } 116 117 if !foundVolume { 118 return errors.Wrapf(ErrNotVolumePlugin, "plugin %s does not implement volume plugin, instead provides %s", newPlugin.Name, strings.Join(respStruct.Implements, ", ")) 119 } 120 121 if plugins == nil { 122 plugins = make(map[string]*VolumePlugin) 123 } 124 125 plugins[newPlugin.Name] = newPlugin 126 127 return nil 128 } 129 130 // GetVolumePlugin gets a single volume plugin, with the given name, at the 131 // given path. 132 func GetVolumePlugin(name string, path string) (*VolumePlugin, error) { 133 pluginsLock.Lock() 134 defer pluginsLock.Unlock() 135 136 plugin, exists := plugins[name] 137 if exists { 138 // This shouldn't be possible, but just in case... 139 if plugin.SocketPath != filepath.Clean(path) { 140 return nil, errors.Wrapf(define.ErrInvalidArg, "requested path %q for volume plugin %s does not match pre-existing path for plugin, %q", path, name, plugin.SocketPath) 141 } 142 143 return plugin, nil 144 } 145 146 // It's not cached. We need to get it. 147 148 newPlugin := new(VolumePlugin) 149 newPlugin.Name = name 150 newPlugin.SocketPath = filepath.Clean(path) 151 152 // Need an HTTP client to force a Unix connection. 153 // And since we can reuse it, might as well cache it. 154 client := new(http.Client) 155 client.Timeout = defaultTimeout 156 // This bit borrowed from pkg/bindings/connection.go 157 client.Transport = &http.Transport{ 158 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { 159 return (&net.Dialer{}).DialContext(ctx, "unix", newPlugin.SocketPath) 160 }, 161 DisableCompression: true, 162 } 163 newPlugin.Client = client 164 165 stat, err := os.Stat(newPlugin.SocketPath) 166 if err != nil { 167 return nil, errors.Wrapf(err, "cannot access plugin %s socket %q", name, newPlugin.SocketPath) 168 } 169 if stat.Mode()&os.ModeSocket == 0 { 170 return nil, errors.Wrapf(ErrNotPlugin, "volume %s path %q is not a unix socket", name, newPlugin.SocketPath) 171 } 172 173 if err := validatePlugin(newPlugin); err != nil { 174 return nil, err 175 } 176 177 return newPlugin, nil 178 } 179 180 func (p *VolumePlugin) getURI() string { 181 return "unix://" + p.SocketPath 182 } 183 184 // Verify the plugin is still available. 185 // Does not actually ping the API, just verifies that the socket still exists. 186 func (p *VolumePlugin) verifyReachable() error { 187 if _, err := os.Stat(p.SocketPath); err != nil { 188 if os.IsNotExist(err) { 189 pluginsLock.Lock() 190 defer pluginsLock.Unlock() 191 delete(plugins, p.Name) 192 return errors.Wrapf(ErrPluginRemoved, p.Name) 193 } 194 195 return errors.Wrapf(err, "error accessing plugin %s", p.Name) 196 } 197 return nil 198 } 199 200 // Send a request to the volume plugin for handling. 201 // Callers *MUST* close the response when they are done. 202 func (p *VolumePlugin) sendRequest(toJSON interface{}, hasBody bool, endpoint string) (*http.Response, error) { 203 var ( 204 reqJSON []byte 205 err error 206 ) 207 208 if hasBody { 209 reqJSON, err = json.Marshal(toJSON) 210 if err != nil { 211 return nil, errors.Wrapf(err, "error marshalling request JSON for volume plugin %s endpoint %s", p.Name, endpoint) 212 } 213 } 214 215 req, err := http.NewRequest("POST", "http://plugin"+endpoint, bytes.NewReader(reqJSON)) 216 if err != nil { 217 return nil, errors.Wrapf(err, "error making request to volume plugin %s endpoint %s", p.Name, endpoint) 218 } 219 220 req.Header.Set("Host", p.getURI()) 221 req.Header.Set("Content-Type", sdk.DefaultContentTypeV1_1) 222 223 resp, err := p.Client.Do(req) 224 if err != nil { 225 return nil, errors.Wrapf(err, "error sending request to volume plugin %s endpoint %s", p.Name, endpoint) 226 } 227 // We are *deliberately not closing* response here. It is the 228 // responsibility of the caller to do so after reading the response. 229 230 return resp, nil 231 } 232 233 // Turn an error response from a volume plugin into a well-formatted Go error. 234 func (p *VolumePlugin) makeErrorResponse(err, endpoint, volName string) error { 235 if err == "" { 236 err = "empty error from plugin" 237 } 238 if volName != "" { 239 return errors.Wrapf(errors.New(err), "error on %s on volume %s in volume plugin %s", endpoint, volName, p.Name) 240 } 241 return errors.Wrapf(errors.New(err), "error on %s in volume plugin %s", endpoint, p.Name) 242 } 243 244 // Handle error responses from plugin 245 func (p *VolumePlugin) handleErrorResponse(resp *http.Response, endpoint, volName string) error { 246 // The official plugin reference implementation uses HTTP 500 for 247 // errors, but I don't think we can guarantee all plugins do that. 248 // Let's interpret anything other than 200 as an error. 249 // If there isn't an error, don't even bother decoding the response. 250 if resp.StatusCode != 200 { 251 errResp, err := ioutil.ReadAll(resp.Body) 252 if err != nil { 253 return errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) 254 } 255 256 errStruct := new(volume.ErrorResponse) 257 if err := json.Unmarshal(errResp, errStruct); err != nil { 258 return errors.Wrapf(err, "error unmarshalling JSON response from volume plugin %s", p.Name) 259 } 260 261 return p.makeErrorResponse(errStruct.Err, endpoint, volName) 262 } 263 264 return nil 265 } 266 267 // CreateVolume creates a volume in the plugin. 268 func (p *VolumePlugin) CreateVolume(req *volume.CreateRequest) error { 269 if req == nil { 270 return errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to CreateVolume") 271 } 272 273 if err := p.verifyReachable(); err != nil { 274 return err 275 } 276 277 logrus.Infof("Creating volume %s using plugin %s", req.Name, p.Name) 278 279 resp, err := p.sendRequest(req, true, createPath) 280 if err != nil { 281 return err 282 } 283 defer resp.Body.Close() 284 285 return p.handleErrorResponse(resp, createPath, req.Name) 286 } 287 288 // ListVolumes lists volumes available in the plugin. 289 func (p *VolumePlugin) ListVolumes() ([]*volume.Volume, error) { 290 if err := p.verifyReachable(); err != nil { 291 return nil, err 292 } 293 294 logrus.Infof("Listing volumes using plugin %s", p.Name) 295 296 resp, err := p.sendRequest(nil, false, listPath) 297 if err != nil { 298 return nil, err 299 } 300 defer resp.Body.Close() 301 302 if err := p.handleErrorResponse(resp, listPath, ""); err != nil { 303 return nil, err 304 } 305 306 volumeRespBytes, err := ioutil.ReadAll(resp.Body) 307 if err != nil { 308 return nil, errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) 309 } 310 311 volumeResp := new(volume.ListResponse) 312 if err := json.Unmarshal(volumeRespBytes, volumeResp); err != nil { 313 return nil, errors.Wrapf(err, "error unmarshalling volume plugin %s list response", p.Name) 314 } 315 316 return volumeResp.Volumes, nil 317 } 318 319 // GetVolume gets a single volume from the plugin. 320 func (p *VolumePlugin) GetVolume(req *volume.GetRequest) (*volume.Volume, error) { 321 if req == nil { 322 return nil, errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to GetVolume") 323 } 324 325 if err := p.verifyReachable(); err != nil { 326 return nil, err 327 } 328 329 logrus.Infof("Getting volume %s using plugin %s", req.Name, p.Name) 330 331 resp, err := p.sendRequest(req, true, getPath) 332 if err != nil { 333 return nil, err 334 } 335 defer resp.Body.Close() 336 337 if err := p.handleErrorResponse(resp, getPath, req.Name); err != nil { 338 return nil, err 339 } 340 341 getRespBytes, err := ioutil.ReadAll(resp.Body) 342 if err != nil { 343 return nil, errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) 344 } 345 346 getResp := new(volume.GetResponse) 347 if err := json.Unmarshal(getRespBytes, getResp); err != nil { 348 return nil, errors.Wrapf(err, "error unmarshalling volume plugin %s get response", p.Name) 349 } 350 351 return getResp.Volume, nil 352 } 353 354 // RemoveVolume removes a single volume from the plugin. 355 func (p *VolumePlugin) RemoveVolume(req *volume.RemoveRequest) error { 356 if req == nil { 357 return errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to RemoveVolume") 358 } 359 360 if err := p.verifyReachable(); err != nil { 361 return err 362 } 363 364 logrus.Infof("Removing volume %s using plugin %s", req.Name, p.Name) 365 366 resp, err := p.sendRequest(req, true, removePath) 367 if err != nil { 368 return err 369 } 370 defer resp.Body.Close() 371 372 return p.handleErrorResponse(resp, removePath, req.Name) 373 } 374 375 // GetVolumePath gets the path the given volume is mounted at. 376 func (p *VolumePlugin) GetVolumePath(req *volume.PathRequest) (string, error) { 377 if req == nil { 378 return "", errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to GetVolumePath") 379 } 380 381 if err := p.verifyReachable(); err != nil { 382 return "", err 383 } 384 385 logrus.Infof("Getting volume %s path using plugin %s", req.Name, p.Name) 386 387 resp, err := p.sendRequest(req, true, hostVirtualPath) 388 if err != nil { 389 return "", err 390 } 391 defer resp.Body.Close() 392 393 if err := p.handleErrorResponse(resp, hostVirtualPath, req.Name); err != nil { 394 return "", err 395 } 396 397 pathRespBytes, err := ioutil.ReadAll(resp.Body) 398 if err != nil { 399 return "", errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) 400 } 401 402 pathResp := new(volume.PathResponse) 403 if err := json.Unmarshal(pathRespBytes, pathResp); err != nil { 404 return "", errors.Wrapf(err, "error unmarshalling volume plugin %s path response", p.Name) 405 } 406 407 return pathResp.Mountpoint, nil 408 } 409 410 // MountVolume mounts the given volume. The ID argument is the ID of the 411 // mounting container, used for internal record-keeping by the plugin. Returns 412 // the path the volume has been mounted at. 413 func (p *VolumePlugin) MountVolume(req *volume.MountRequest) (string, error) { 414 if req == nil { 415 return "", errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to MountVolume") 416 } 417 418 if err := p.verifyReachable(); err != nil { 419 return "", err 420 } 421 422 logrus.Infof("Mounting volume %s using plugin %s for container %s", req.Name, p.Name, req.ID) 423 424 resp, err := p.sendRequest(req, true, mountPath) 425 if err != nil { 426 return "", err 427 } 428 defer resp.Body.Close() 429 430 if err := p.handleErrorResponse(resp, mountPath, req.Name); err != nil { 431 return "", err 432 } 433 434 mountRespBytes, err := ioutil.ReadAll(resp.Body) 435 if err != nil { 436 return "", errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) 437 } 438 439 mountResp := new(volume.MountResponse) 440 if err := json.Unmarshal(mountRespBytes, mountResp); err != nil { 441 return "", errors.Wrapf(err, "error unmarshalling volume plugin %s path response", p.Name) 442 } 443 444 return mountResp.Mountpoint, nil 445 } 446 447 // UnmountVolume unmounts the given volume. The ID argument is the ID of the 448 // container that is unmounting, used for internal record-keeping by the plugin. 449 func (p *VolumePlugin) UnmountVolume(req *volume.UnmountRequest) error { 450 if req == nil { 451 return errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to UnmountVolume") 452 } 453 454 if err := p.verifyReachable(); err != nil { 455 return err 456 } 457 458 logrus.Infof("Unmounting volume %s using plugin %s for container %s", req.Name, p.Name, req.ID) 459 460 resp, err := p.sendRequest(req, true, unmountPath) 461 if err != nil { 462 return err 463 } 464 defer resp.Body.Close() 465 466 return p.handleErrorResponse(resp, unmountPath, req.Name) 467 }