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  }