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  }