diff options
Diffstat (limited to 'libpod')
-rw-r--r-- | libpod/network/create.go | 52 | ||||
-rw-r--r-- | libpod/network/files.go | 48 | ||||
-rw-r--r-- | libpod/network/netconflist.go | 92 | ||||
-rw-r--r-- | libpod/network/network.go | 11 | ||||
-rw-r--r-- | libpod/options.go | 13 | ||||
-rw-r--r-- | libpod/plugin/volume_api.go | 454 | ||||
-rw-r--r-- | libpod/runtime_img.go | 52 | ||||
-rw-r--r-- | libpod/runtime_pod_infra_linux.go | 67 |
8 files changed, 719 insertions, 70 deletions
diff --git a/libpod/network/create.go b/libpod/network/create.go index 7e4fc574a..094fbe349 100644 --- a/libpod/network/create.go +++ b/libpod/network/create.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strconv" "github.com/containernetworking/cni/pkg/version" "github.com/containers/common/pkg/config" @@ -76,6 +77,29 @@ func validateBridgeOptions(options entities.NetworkCreateOptions) error { } +// parseMTU parses the mtu option +func parseMTU(mtu string) (int, error) { + if mtu == "" { + return 0, nil // default + } + m, err := strconv.Atoi(mtu) + if err != nil { + return 0, err + } + if m < 0 { + return 0, errors.Errorf("the value %d for mtu is less than zero", m) + } + return m, nil +} + +// parseVlan parses the vlan option +func parseVlan(vlan string) (int, error) { + if vlan == "" { + return 0, nil // default + } + return strconv.Atoi(vlan) +} + // createBridge creates a CNI network func createBridge(name string, options entities.NetworkCreateOptions, runtimeConfig *config.Config) (string, error) { var ( @@ -149,6 +173,28 @@ func createBridge(name string, options entities.NetworkCreateOptions, runtimeCon ipMasq = false } + var mtu int + var vlan int + for k, v := range options.Options { + var err error + switch k { + case "mtu": + mtu, err = parseMTU(v) + if err != nil { + return "", err + } + + case "vlan": + vlan, err = parseVlan(v) + if err != nil { + return "", err + } + + default: + return "", errors.Errorf("unsupported option %s", k) + } + } + // obtain host bridge name bridgeDeviceName, err := GetFreeDeviceName(runtimeConfig) if err != nil { @@ -169,10 +215,10 @@ func createBridge(name string, options entities.NetworkCreateOptions, runtimeCon } // create CNI plugin configuration - ncList := NewNcList(name, version.Current()) + ncList := NewNcList(name, version.Current(), options.Labels) var plugins []CNIPlugins // TODO need to iron out the role of isDefaultGW and IPMasq - bridge := NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, ipamConfig) + bridge := NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, mtu, vlan, ipamConfig) plugins = append(plugins, bridge) plugins = append(plugins, NewPortMapPlugin()) plugins = append(plugins, NewFirewallPlugin()) @@ -223,7 +269,7 @@ func createMacVLAN(name string, options entities.NetworkCreateOptions, runtimeCo return "", err } } - ncList := NewNcList(name, version.Current()) + ncList := NewNcList(name, version.Current(), options.Labels) macvlan := NewMacVLANPlugin(options.MacVLAN) plugins = append(plugins, macvlan) ncList["plugins"] = plugins diff --git a/libpod/network/files.go b/libpod/network/files.go index 7f1e3ee18..33cf01064 100644 --- a/libpod/network/files.go +++ b/libpod/network/files.go @@ -12,6 +12,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v2/libpod/define" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) // ErrNoSuchNetworkInterface indicates that no network interface exists @@ -49,13 +50,15 @@ func LoadCNIConfsFromDir(dir string) ([]*libcni.NetworkConfigList, error) { return configs, nil } -// GetCNIConfigPathByName finds a CNI network by name and +// GetCNIConfigPathByNameOrID finds a CNI network by name and // returns its configuration file path -func GetCNIConfigPathByName(config *config.Config, name string) (string, error) { +func GetCNIConfigPathByNameOrID(config *config.Config, name string) (string, error) { files, err := libcni.ConfFiles(GetCNIConfDir(config), []string{".conflist"}) if err != nil { return "", err } + idMatch := 0 + file := "" for _, confFile := range files { conf, err := libcni.ConfListFromFile(confFile) if err != nil { @@ -64,6 +67,16 @@ func GetCNIConfigPathByName(config *config.Config, name string) (string, error) if conf.Name == name { return confFile, nil } + if strings.HasPrefix(GetNetworkID(conf.Name), name) { + idMatch++ + file = confFile + } + } + if idMatch == 1 { + return file, nil + } + if idMatch > 1 { + return "", errors.Errorf("more than one result for network ID %s", name) } return "", errors.Wrap(define.ErrNoSuchNetwork, fmt.Sprintf("unable to find network configuration for %s", name)) } @@ -71,7 +84,7 @@ func GetCNIConfigPathByName(config *config.Config, name string) (string, error) // ReadRawCNIConfByName reads the raw CNI configuration for a CNI // network by name func ReadRawCNIConfByName(config *config.Config, name string) ([]byte, error) { - confFile, err := GetCNIConfigPathByName(config, name) + confFile, err := GetCNIConfigPathByNameOrID(config, name) if err != nil { return nil, err } @@ -89,6 +102,35 @@ func GetCNIPlugins(list *libcni.NetworkConfigList) string { return strings.Join(plugins, ",") } +// GetNetworkLabels returns a list of labels as a string +func GetNetworkLabels(list *libcni.NetworkConfigList) NcLabels { + cniJSON := make(map[string]interface{}) + err := json.Unmarshal(list.Bytes, &cniJSON) + if err != nil { + logrus.Errorf("failed to unmarshal network config %v %v", cniJSON["name"], err) + return nil + } + if args, ok := cniJSON["args"]; ok { + if key, ok := args.(map[string]interface{}); ok { + if labels, ok := key[PodmanLabelKey]; ok { + if labels, ok := labels.(map[string]interface{}); ok { + result := make(NcLabels, len(labels)) + for k, v := range labels { + if v, ok := v.(string); ok { + result[k] = v + } else { + logrus.Errorf("network config %v invalid label value type %T should be string", cniJSON["name"], labels) + } + } + return result + } + logrus.Errorf("network config %v invalid label type %T should be map[string]string", cniJSON["name"], labels) + } + } + } + return nil +} + // GetNetworksFromFilesystem gets all the networks from the cni configuration // files func GetNetworksFromFilesystem(config *config.Config) ([]*allocator.Net, error) { diff --git a/libpod/network/netconflist.go b/libpod/network/netconflist.go index ee9adce14..d61b96ecb 100644 --- a/libpod/network/netconflist.go +++ b/libpod/network/netconflist.go @@ -4,6 +4,11 @@ import ( "net" "os" "path/filepath" + "strings" + + "github.com/containernetworking/cni/libcni" + "github.com/containers/podman/v2/pkg/util" + "github.com/pkg/errors" ) const ( @@ -14,22 +19,36 @@ const ( // NcList describes a generic map type NcList map[string]interface{} +// NcArgs describes the cni args field +type NcArgs map[string]NcLabels + +// NcLabels describes the label map +type NcLabels map[string]string + +// PodmanLabelKey key used to store the podman network label in a cni config +const PodmanLabelKey = "podman_labels" + // NewNcList creates a generic map of values with string // keys and adds in version and network name -func NewNcList(name, version string) NcList { +func NewNcList(name, version string, labels NcLabels) NcList { n := NcList{} n["cniVersion"] = version n["name"] = name + if len(labels) > 0 { + n["args"] = NcArgs{PodmanLabelKey: labels} + } return n } // NewHostLocalBridge creates a new LocalBridge for host-local -func NewHostLocalBridge(name string, isGateWay, isDefaultGW, ipMasq bool, ipamConf IPAMHostLocalConf) *HostLocalBridge { +func NewHostLocalBridge(name string, isGateWay, isDefaultGW, ipMasq bool, mtu int, vlan int, ipamConf IPAMHostLocalConf) *HostLocalBridge { hostLocalBridge := HostLocalBridge{ PluginType: "bridge", BrName: name, IPMasq: ipMasq, + MTU: mtu, HairpinMode: true, + Vlan: vlan, IPAM: ipamConf, } if isGateWay { @@ -159,3 +178,72 @@ func NewMacVLANPlugin(device string) MacVLANConfig { } return m } + +// IfPassesFilter filters NetworkListReport and returns true if the filter match the given config +func IfPassesFilter(netconf *libcni.NetworkConfigList, filters map[string][]string) (bool, error) { + result := true + for key, filterValues := range filters { + result = false + switch strings.ToLower(key) { + case "name": + // matches one name, regex allowed + result = util.StringMatchRegexSlice(netconf.Name, filterValues) + + case "plugin": + // match one plugin + plugins := GetCNIPlugins(netconf) + for _, val := range filterValues { + if strings.Contains(plugins, val) { + result = true + break + } + } + + case "label": + // matches all labels + labels := GetNetworkLabels(netconf) + outer: + for _, filterValue := range filterValues { + filterArray := strings.SplitN(filterValue, "=", 2) + filterKey := filterArray[0] + if len(filterArray) > 1 { + filterValue = filterArray[1] + } else { + filterValue = "" + } + for labelKey, labelValue := range labels { + if labelKey == filterKey && ("" == filterValue || labelValue == filterValue) { + result = true + continue outer + } + } + result = false + } + + case "driver": + // matches only for the DefaultNetworkDriver + for _, filterValue := range filterValues { + plugins := GetCNIPlugins(netconf) + if filterValue == DefaultNetworkDriver && + strings.Contains(plugins, DefaultNetworkDriver) { + result = true + } + } + + case "id": + // matches part of one id + for _, filterValue := range filterValues { + if strings.Contains(GetNetworkID(netconf.Name), filterValue) { + result = true + break + } + } + + // TODO: add dangling filter + + default: + return false, errors.Errorf("invalid filter %q", key) + } + } + return result, nil +} diff --git a/libpod/network/network.go b/libpod/network/network.go index 0febb52f6..89f0b67ac 100644 --- a/libpod/network/network.go +++ b/libpod/network/network.go @@ -1,6 +1,8 @@ package network import ( + "crypto/sha256" + "encoding/hex" "encoding/json" "net" "os" @@ -175,7 +177,7 @@ func RemoveNetwork(config *config.Config, name string) error { return err } defer l.releaseCNILock() - cniPath, err := GetCNIConfigPathByName(config, name) + cniPath, err := GetCNIConfigPathByNameOrID(config, name) if err != nil { return err } @@ -229,3 +231,10 @@ func Exists(config *config.Config, name string) (bool, error) { } return true, nil } + +// GetNetworkID return the network ID for a given name. +// It is just the sha256 hash but this should be good enough. +func GetNetworkID(name string) string { + hash := sha256.Sum256([]byte(name)) + return hex.EncodeToString(hash[:]) +} diff --git a/libpod/options.go b/libpod/options.go index 0f55f34a3..bd12c0c34 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1547,12 +1547,19 @@ func WithVolumeDriver(driver string) VolumeCreateOption { if volume.valid { return define.ErrVolumeFinalized } - // only local driver is possible rn + + // Uncomment when volume plugins are ready for use. + // if driver != define.VolumeDriverLocal { + // if _, err := plugin.GetVolumePlugin(driver); err != nil { + // return err + // } + // } + if driver != define.VolumeDriverLocal { return define.ErrNotImplemented - } - volume.config.Driver = define.VolumeDriverLocal + + volume.config.Driver = driver return nil } } diff --git a/libpod/plugin/volume_api.go b/libpod/plugin/volume_api.go new file mode 100644 index 000000000..2500a4f36 --- /dev/null +++ b/libpod/plugin/volume_api.go @@ -0,0 +1,454 @@ +package plugin + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/containers/podman/v2/libpod/define" + "github.com/docker/go-plugins-helpers/sdk" + "github.com/docker/go-plugins-helpers/volume" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +// TODO: We should add syntax for specifying plugins to containers.conf, and +// support for loading based on that. + +// Copied from docker/go-plugins-helpers/volume/api.go - not exported, so we +// need to do this to get at them. +// These are well-established paths that should not change unless the plugin API +// version changes. +var ( + activatePath = "/Plugin.Activate" + createPath = "/VolumeDriver.Create" + getPath = "/VolumeDriver.Get" + listPath = "/VolumeDriver.List" + removePath = "/VolumeDriver.Remove" + hostVirtualPath = "/VolumeDriver.Path" + mountPath = "/VolumeDriver.Mount" + unmountPath = "/VolumeDriver.Unmount" + // nolint + capabilitiesPath = "/VolumeDriver.Capabilities" +) + +const ( + defaultTimeout = 5 * time.Second + defaultPath = "/run/docker/plugins" + volumePluginType = "VolumeDriver" +) + +var ( + ErrNotPlugin = errors.New("target does not appear to be a valid plugin") + ErrNotVolumePlugin = errors.New("plugin is not a volume plugin") + ErrPluginRemoved = errors.New("plugin is no longer available (shut down?)") + + // This stores available, initialized volume plugins. + pluginsLock sync.Mutex + plugins map[string]*VolumePlugin +) + +// VolumePlugin is a single volume plugin. +type VolumePlugin struct { + // Name is the name of the volume plugin. This will be used to refer to + // it. + Name string + // SocketPath is the unix socket at which the plugin is accessed. + SocketPath string +} + +// This is the response from the activate endpoint of the API. +type activateResponse struct { + Implements []string +} + +// Validate that the given plugin is good to use. +// Add it to available plugins if so. +func validatePlugin(newPlugin *VolumePlugin) error { + // It's a socket. Is it a plugin? + // Hit the Activate endpoint to find out if it is, and if so what kind + req, err := http.NewRequest("POST", activatePath, nil) + if err != nil { + return errors.Wrapf(err, "error making request to volume plugin %s activation endpoint", newPlugin.Name) + } + + req.Header.Set("Host", newPlugin.getURI()) + req.Header.Set("Content-Type", sdk.DefaultContentTypeV1_1) + + client := new(http.Client) + client.Timeout = defaultTimeout + resp, err := client.Do(req) + if err != nil { + return errors.Wrapf(err, "error sending request to plugin %s activation endpoint", newPlugin.Name) + } + defer resp.Body.Close() + + // Response code MUST be 200. Anything else, we have to assume it's not + // a valid plugin. + if resp.StatusCode != 200 { + return errors.Wrapf(ErrNotPlugin, "got status code %d from activation endpoint for plugin %s", resp.StatusCode, newPlugin.Name) + } + + // Read and decode the body so we can tell if this is a volume plugin. + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.Wrapf(err, "error reading activation response body from plugin %s", newPlugin.Name) + } + + respStruct := new(activateResponse) + if err := json.Unmarshal(respBytes, respStruct); err != nil { + return errors.Wrapf(err, "error unmarshalling plugin %s activation response", newPlugin.Name) + } + + foundVolume := false + for _, pluginType := range respStruct.Implements { + if pluginType == volumePluginType { + foundVolume = true + break + } + } + + if !foundVolume { + return errors.Wrapf(ErrNotVolumePlugin, "plugin %s does not implement volume plugin, instead provides %s", newPlugin.Name, strings.Join(respStruct.Implements, ", ")) + } + + plugins[newPlugin.Name] = newPlugin + + return nil +} + +// GetVolumePlugin gets a single volume plugin by path. +// TODO: We should not be auto-completing based on a default path; we should +// require volumes to have been pre-specified in containers.conf (will need a +// function to pre-populate the plugins list, and we should probably do a lazy +// initialization there to not slow things down too much). +func GetVolumePlugin(name string) (*VolumePlugin, error) { + pluginsLock.Lock() + defer pluginsLock.Unlock() + + plugin, exists := plugins[name] + if exists { + return plugin, nil + } + + // It's not cached. We need to get it. + + newPlugin := new(VolumePlugin) + newPlugin.Name = name + newPlugin.SocketPath = filepath.Join(defaultPath, fmt.Sprintf("%s.sock", name)) + + stat, err := os.Stat(newPlugin.SocketPath) + if err != nil { + return nil, errors.Wrapf(err, "cannot access plugin %s socket %q", name, newPlugin.SocketPath) + } + if stat.Mode()&os.ModeSocket == 0 { + return nil, errors.Wrapf(ErrNotPlugin, "volume %s path %q is not a unix socket", name, newPlugin.SocketPath) + } + + if err := validatePlugin(newPlugin); err != nil { + return nil, err + } + + return newPlugin, nil +} + +func (p *VolumePlugin) getURI() string { + return "unix://" + p.SocketPath +} + +// Verify the plugin is still available. +// TODO: Do we want to ping with an HTTP request? There's no ping endpoint so +// we'd need to hit Activate or Capabilities? +func (p *VolumePlugin) verifyReachable() error { + if _, err := os.Stat(p.SocketPath); err != nil { + if os.IsNotExist(err) { + pluginsLock.Lock() + defer pluginsLock.Unlock() + delete(plugins, p.Name) + return errors.Wrapf(ErrPluginRemoved, p.Name) + } + + return errors.Wrapf(err, "error accessing plugin %s", p.Name) + } + return nil +} + +// Send a request to the volume plugin for handling. +func (p *VolumePlugin) sendRequest(toJSON interface{}, hasBody bool, endpoint string) (*http.Response, error) { + var ( + reqJSON []byte + err error + ) + + if hasBody { + reqJSON, err = json.Marshal(toJSON) + if err != nil { + return nil, errors.Wrapf(err, "error marshalling request JSON for volume plugin %s endpoint %s", p.Name, endpoint) + } + } + + req, err := http.NewRequest("POST", endpoint, bytes.NewReader(reqJSON)) + if err != nil { + return nil, errors.Wrapf(err, "error making request to volume plugin %s endpoint %s", p.Name, endpoint) + } + + req.Header.Set("Host", p.getURI()) + req.Header.Set("Content-Type", sdk.DefaultContentTypeV1_1) + + client := new(http.Client) + client.Timeout = defaultTimeout + resp, err := client.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "error sending request to volume plugin %s endpoint %s", p.Name, endpoint) + } + defer resp.Body.Close() + + return resp, nil +} + +// Turn an error response from a volume plugin into a well-formatted Go error. +func (p *VolumePlugin) makeErrorResponse(err, endpoint, volName string) error { + if err == "" { + err = "empty error from plugin" + } + if volName != "" { + return errors.Wrapf(errors.New(err), "error on %s on volume %s in volume plugin %s", endpoint, volName, p.Name) + } else { + return errors.Wrapf(errors.New(err), "error on %s in volume plugin %s", endpoint, p.Name) + } +} + +// Handle error responses from plugin +func (p *VolumePlugin) handleErrorResponse(resp *http.Response, endpoint, volName string) error { + // The official plugin reference implementation uses HTTP 500 for + // errors, but I don't think we can guarantee all plugins do that. + // Let's interpret anything other than 200 as an error. + // If there isn't an error, don't even bother decoding the response. + if resp.StatusCode != 200 { + errResp, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) + } + + errStruct := new(volume.ErrorResponse) + if err := json.Unmarshal(errResp, errStruct); err != nil { + return errors.Wrapf(err, "error unmarshalling JSON response from volume plugin %s", p.Name) + } + + return p.makeErrorResponse(errStruct.Err, endpoint, volName) + } + + return nil +} + +// CreateVolume creates a volume in the plugin. +func (p *VolumePlugin) CreateVolume(req *volume.CreateRequest) error { + if req == nil { + return errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to CreateVolume") + } + + if err := p.verifyReachable(); err != nil { + return err + } + + logrus.Infof("Creating volume %s using plugin %s", req.Name, p.Name) + + resp, err := p.sendRequest(req, true, createPath) + if err != nil { + return err + } + defer resp.Body.Close() + + return p.handleErrorResponse(resp, createPath, req.Name) +} + +// ListVolumes lists volumes available in the plugin. +func (p *VolumePlugin) ListVolumes() ([]*volume.Volume, error) { + if err := p.verifyReachable(); err != nil { + return nil, err + } + + logrus.Infof("Listing volumes using plugin %s", p.Name) + + resp, err := p.sendRequest(nil, false, listPath) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := p.handleErrorResponse(resp, listPath, ""); err != nil { + return nil, err + } + + // TODO: Can probably unify response reading under a helper + volumeRespBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) + } + + volumeResp := new(volume.ListResponse) + if err := json.Unmarshal(volumeRespBytes, volumeResp); err != nil { + return nil, errors.Wrapf(err, "error unmarshalling volume plugin %s list response", p.Name) + } + + return volumeResp.Volumes, nil +} + +// GetVolume gets a single volume from the plugin. +func (p *VolumePlugin) GetVolume(req *volume.GetRequest) (*volume.Volume, error) { + if req == nil { + return nil, errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to GetVolume") + } + + if err := p.verifyReachable(); err != nil { + return nil, err + } + + logrus.Infof("Getting volume %s using plugin %s", req.Name, p.Name) + + resp, err := p.sendRequest(req, true, getPath) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := p.handleErrorResponse(resp, getPath, req.Name); err != nil { + return nil, err + } + + getRespBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) + } + + getResp := new(volume.GetResponse) + if err := json.Unmarshal(getRespBytes, getResp); err != nil { + return nil, errors.Wrapf(err, "error unmarshalling volume plugin %s get response", p.Name) + } + + return getResp.Volume, nil +} + +// RemoveVolume removes a single volume from the plugin. +func (p *VolumePlugin) RemoveVolume(req *volume.RemoveRequest) error { + if req == nil { + return errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to RemoveVolume") + } + + if err := p.verifyReachable(); err != nil { + return err + } + + logrus.Infof("Removing volume %s using plugin %s", req.Name, p.Name) + + resp, err := p.sendRequest(req, true, removePath) + if err != nil { + return err + } + defer resp.Body.Close() + + return p.handleErrorResponse(resp, removePath, req.Name) +} + +// GetVolumePath gets the path the given volume is mounted at. +func (p *VolumePlugin) GetVolumePath(req *volume.PathRequest) (string, error) { + if req == nil { + return "", errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to GetVolumePath") + } + + if err := p.verifyReachable(); err != nil { + return "", err + } + + logrus.Infof("Getting volume %s path using plugin %s", req.Name, p.Name) + + resp, err := p.sendRequest(req, true, hostVirtualPath) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if err := p.handleErrorResponse(resp, hostVirtualPath, req.Name); err != nil { + return "", err + } + + pathRespBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) + } + + pathResp := new(volume.PathResponse) + if err := json.Unmarshal(pathRespBytes, pathResp); err != nil { + return "", errors.Wrapf(err, "error unmarshalling volume plugin %s path response", p.Name) + } + + return pathResp.Mountpoint, nil +} + +// MountVolume mounts the given volume. The ID argument is the ID of the +// mounting container, used for internal record-keeping by the plugin. Returns +// the path the volume has been mounted at. +func (p *VolumePlugin) MountVolume(req *volume.MountRequest) (string, error) { + if req == nil { + return "", errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to MountVolume") + } + + if err := p.verifyReachable(); err != nil { + return "", err + } + + logrus.Infof("Mounting volume %s using plugin %s for container %s", req.Name, p.Name, req.ID) + + resp, err := p.sendRequest(req, true, mountPath) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if err := p.handleErrorResponse(resp, mountPath, req.Name); err != nil { + return "", err + } + + mountRespBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) + } + + mountResp := new(volume.MountResponse) + if err := json.Unmarshal(mountRespBytes, mountResp); err != nil { + return "", errors.Wrapf(err, "error unmarshalling volume plugin %s path response", p.Name) + } + + return mountResp.Mountpoint, nil +} + +// UnmountVolume unmounts the given volume. The ID argument is the ID of the +// container that is unmounting, used for internal record-keeping by the plugin. +func (p *VolumePlugin) UnmountVolume(req *volume.UnmountRequest) error { + if req == nil { + return errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to UnmountVolume") + } + + if err := p.verifyReachable(); err != nil { + return err + } + + logrus.Infof("Unmounting volume %s using plugin %s for container %s", req.Name, p.Name, req.ID) + + resp, err := p.sendRequest(req, true, unmountPath) + if err != nil { + return err + } + defer resp.Body.Close() + + return p.handleErrorResponse(resp, unmountPath, req.Name) +} diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go index e57890fa2..a2d9a875e 100644 --- a/libpod/runtime_img.go +++ b/libpod/runtime_img.go @@ -8,7 +8,6 @@ import ( "net/http" "net/url" "os" - "strings" "github.com/containers/buildah/imagebuildah" "github.com/containers/image/v5/directory" @@ -276,56 +275,47 @@ func DownloadFromFile(reader *os.File) (string, error) { } // LoadImage loads a container image into local storage -func (r *Runtime) LoadImage(ctx context.Context, name, inputFile string, writer io.Writer, signaturePolicy string) (string, error) { - var ( - newImages []*image.Image - err error - src types.ImageReference - ) +func (r *Runtime) LoadImage(ctx context.Context, inputFile string, writer io.Writer, signaturePolicy string) (string, error) { + if newImages, err := r.LoadAllImageFromArchive(ctx, writer, inputFile, signaturePolicy); err == nil { + return newImages, nil + } + return r.LoadImageFromSingleImageArchive(ctx, writer, inputFile, signaturePolicy) +} - if name == "" { - newImages, err = r.ImageRuntime().LoadAllImagesFromDockerArchive(ctx, inputFile, signaturePolicy, writer) - if err == nil { - return getImageNames(newImages), nil - } +// LoadAllImageFromArchive loads all images from the archive of multi-image that inputFile points to. +func (r *Runtime) LoadAllImageFromArchive(ctx context.Context, writer io.Writer, inputFile, signaturePolicy string) (string, error) { + newImages, err := r.ImageRuntime().LoadAllImagesFromDockerArchive(ctx, inputFile, signaturePolicy, writer) + if err == nil { + return getImageNames(newImages), nil } + return "", err +} +// LoadImageFromSingleImageArchive load image from the archive of single image that inputFile points to. +func (r *Runtime) LoadImageFromSingleImageArchive(ctx context.Context, writer io.Writer, inputFile, signaturePolicy string) (string, error) { + var err error for _, referenceFn := range []func() (types.ImageReference, error){ func() (types.ImageReference, error) { return dockerarchive.ParseReference(inputFile) }, func() (types.ImageReference, error) { - return ociarchive.NewReference(inputFile, name) // name may be "" - }, - func() (types.ImageReference, error) { - // prepend "localhost/" to support local image saved with this semantics - if !strings.Contains(name, "/") { - return ociarchive.NewReference(inputFile, fmt.Sprintf("%s/%s", image.DefaultLocalRegistry, name)) - } - return nil, nil + return ociarchive.NewReference(inputFile, "") }, func() (types.ImageReference, error) { return directory.NewReference(inputFile) }, func() (types.ImageReference, error) { - return layout.NewReference(inputFile, name) - }, - func() (types.ImageReference, error) { - // prepend "localhost/" to support local image saved with this semantics - if !strings.Contains(name, "/") { - return layout.NewReference(inputFile, fmt.Sprintf("%s/%s", image.DefaultLocalRegistry, name)) - } - return nil, nil + return layout.NewReference(inputFile, "") }, } { - src, err = referenceFn() + src, err := referenceFn() if err == nil && src != nil { - if newImages, err = r.ImageRuntime().LoadFromArchiveReference(ctx, src, signaturePolicy, writer); err == nil { + if newImages, err := r.ImageRuntime().LoadFromArchiveReference(ctx, src, signaturePolicy, writer); err == nil { return getImageNames(newImages), nil } } } - return "", errors.Wrapf(err, "error pulling %q", name) + return "", errors.Wrapf(err, "error pulling image") } func getImageNames(images []*image.Image) string { diff --git a/libpod/runtime_pod_infra_linux.go b/libpod/runtime_pod_infra_linux.go index 76419587a..3e4185db1 100644 --- a/libpod/runtime_pod_infra_linux.go +++ b/libpod/runtime_pod_infra_linux.go @@ -34,40 +34,56 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, rawIm // Set Pod hostname g.Config.Hostname = p.config.Hostname + var options []CtrCreateOption + + // Command: If user-specified, use that preferentially. + // If not set and the config file is set, fall back to that. + var infraCtrCommand []string + if p.config.InfraContainer.InfraCommand != nil { + logrus.Debugf("User-specified infra container entrypoint %v", p.config.InfraContainer.InfraCommand) + infraCtrCommand = p.config.InfraContainer.InfraCommand + } else if r.config.Engine.InfraCommand != "" { + logrus.Debugf("Config-specified infra container entrypoint %s", r.config.Engine.InfraCommand) + infraCtrCommand = []string{r.config.Engine.InfraCommand} + } + // Only if set by the user or containers.conf, we set entrypoint for the + // infra container. + // This is only used by commit, so it shouldn't matter... But someone + // may eventually want to commit an infra container? + // TODO: Should we actually do this if set by containers.conf? + if infraCtrCommand != nil { + // Need to duplicate the array - we are going to add Cmd later + // so the current array will be changed. + newArr := make([]string, 0, len(infraCtrCommand)) + newArr = append(newArr, infraCtrCommand...) + options = append(options, WithEntrypoint(newArr)) + } + isRootless := rootless.IsRootless() - entrypointSet := len(p.config.InfraContainer.InfraCommand) > 0 - entryPoint := p.config.InfraContainer.InfraCommand - entryCmd := []string{} - var options []CtrCreateOption // I've seen circumstances where config is being passed as nil. // Let's err on the side of safety and make sure it's safe to use. if config != nil { - // default to entrypoint in image if there is one - if !entrypointSet { - if len(config.Entrypoint) > 0 { - entrypointSet = true - entryPoint = config.Entrypoint - entryCmd = config.Entrypoint + if infraCtrCommand == nil { + // If we have no entrypoint and command from the image, + // we can't go on - the infra container has no command. + if len(config.Entrypoint) == 0 && len(config.Cmd) == 0 { + return nil, errors.Errorf("infra container has no command") } - } else { // so use the InfraCommand - entrypointSet = true - entryCmd = entryPoint - } - - if len(config.Cmd) > 0 { - // We can't use the default pause command, since we're - // sourcing from the image. If we didn't already set an - // entrypoint, set one now. - if !entrypointSet { + if len(config.Entrypoint) > 0 { + infraCtrCommand = config.Entrypoint + } else { // Use the Docker default "/bin/sh -c" // entrypoint, as we're overriding command. // If an image doesn't want this, it can // override entrypoint too. - entryCmd = []string{"/bin/sh", "-c"} + infraCtrCommand = []string{"/bin/sh", "-c"} } - entryCmd = append(entryCmd, config.Cmd...) } + if len(config.Cmd) > 0 { + infraCtrCommand = append(infraCtrCommand, config.Cmd...) + } + if len(config.Env) > 0 { for _, nameValPair := range config.Env { nameValSlice := strings.Split(nameValPair, "=") @@ -127,9 +143,9 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, rawIm } g.SetRootReadonly(true) - g.SetProcessArgs(entryCmd) + g.SetProcessArgs(infraCtrCommand) - logrus.Debugf("Using %q as infra container entrypoint", entryCmd) + logrus.Debugf("Using %q as infra container command", infraCtrCommand) g.RemoveMount("/dev/shm") if isRootless { @@ -148,9 +164,6 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, rawIm options = append(options, WithRootFSFromImage(imgID, imgName, rawImageName)) options = append(options, WithName(containerName)) options = append(options, withIsInfra()) - if entrypointSet { - options = append(options, WithEntrypoint(entryPoint)) - } if len(p.config.InfraContainer.ConmonPidFile) > 0 { options = append(options, WithConmonPidFile(p.config.InfraContainer.ConmonPidFile)) } |