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