Implement a tool executor

The tool executor is capable of running given tools in chroot either by automatically locating the tools in path using the `env` command or directly if the path is known.

This removes the need for the shell script to wrap commands and reduces number of needed external binaries on the node.
This commit is contained in:
Sami Haahtinen
2024-05-26 11:37:28 +00:00
committed by Sami Haahtinen
parent e2cc8de2fa
commit 78e7a08c0e
11 changed files with 614 additions and 95 deletions

View File

@@ -28,14 +28,6 @@ RUN apk add --no-cache e2fsprogs e2fsprogs-extra xfsprogs xfsprogs-extra blkid u
# Create symbolic link for chroot.sh
WORKDIR /
RUN mkdir /csibin
COPY chroot/chroot.sh /csibin
RUN chmod 777 /csibin/chroot.sh \
&& ln -s /csibin/chroot.sh /csibin/iscsiadm \
&& ln -s /csibin/chroot.sh /csibin/multipath \
&& ln -s /csibin/chroot.sh /csibin/multipathd
ENV PATH="/csibin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Copy and run CSI driver
COPY --from=builder /go/src/synok8scsiplugin/bin/synology-csi-driver synology-csi-driver

View File

@@ -1,12 +0,0 @@
#!/usr/bin/env bash
# This script is only used in the container, see Dockerfile.
DIR="/host" # csi-node mount / of the node to /host in the container
BIN="$(basename "$0")"
if [ -d "$DIR" ]; then
exec chroot $DIR /usr/bin/env -i PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" "$BIN" "$@"
fi
echo -n "Couldn't find hostPath: $DIR in the CSI container"
exit 1

40
main.go
View File

@@ -8,13 +8,15 @@ import (
"os"
"os/signal"
"syscall"
"github.com/spf13/cobra"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/SynologyOpenSource/synology-csi/pkg/driver"
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/common"
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/service"
"github.com/SynologyOpenSource/synology-csi/pkg/logger"
"github.com/SynologyOpenSource/synology-csi/pkg/utils/hostexec"
)
var (
@@ -23,16 +25,21 @@ var (
csiEndpoint = "unix:///var/lib/kubelet/plugins/" + driver.DriverName + "/csi.sock"
csiClientInfoPath = "/etc/synology/client-info.yml"
// Logging
logLevel = "info"
webapiDebug = false
logLevel = "info"
webapiDebug = false
multipathForUC = true
// Locations is tools and directories
chrootDir = "/host"
iscsiadmPath = ""
multipathPath = ""
multipathdPath = ""
)
var rootCmd = &cobra.Command{
Use: "synology-csi-driver",
Short: "Synology CSI Driver",
Use: "synology-csi-driver",
Short: "Synology CSI Driver",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(_ *cobra.Command, _ []string) error {
if webapiDebug {
logger.WebapiDebug = true
logLevel = "debug"
@@ -72,8 +79,21 @@ func driverStart() error {
}
defer dsmService.RemoveAllDsms()
// 2. Create and Run the Driver
drv, err := driver.NewControllerAndNodeDriver(csiNodeID, csiEndpoint, dsmService)
// 2. Create command executor
cmdMap := map[string]string{
"iscsiadm": iscsiadmPath,
"multipath": multipathPath,
"multipathd": multipathdPath,
}
cmdExecutor, err := hostexec.New(cmdMap, chrootDir)
if err != nil {
log.Errorf("Failed to create command executor: %v", err)
return err
}
tools := driver.NewTools(cmdExecutor)
// 3. Create and Run the Driver
drv, err := driver.NewControllerAndNodeDriver(csiNodeID, csiEndpoint, dsmService, tools)
if err != nil {
log.Errorf("Failed to create driver: %v", err)
return err
@@ -105,6 +125,10 @@ func addFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVar(&logLevel, "log-level", logLevel, "Log level (debug, info, warn, error, fatal)")
cmd.PersistentFlags().BoolVarP(&webapiDebug, "debug", "d", webapiDebug, "Enable webapi debugging logs")
cmd.PersistentFlags().BoolVar(&multipathForUC, "multipath", multipathForUC, "Set to 'false' to disable multipath for UC")
cmd.PersistentFlags().StringVar(&chrootDir, "chroot-dir", chrootDir, "Host directory to chroot into (empty disables chroot)")
cmd.PersistentFlags().StringVar(&iscsiadmPath, "iscsiadm-path", iscsiadmPath, "Full path of iscsiadm executable")
cmd.PersistentFlags().StringVar(&multipathPath, "multipath-path", multipathPath, "Full path of multipath executable")
cmd.PersistentFlags().StringVar(&multipathdPath, "multipathd-path", multipathdPath, "Full path of multipathd executable")
cmd.MarkFlagRequired("endpoint")
cmd.MarkFlagRequired("client-info")

View File

@@ -17,19 +17,19 @@ limitations under the License.
package driver
import (
"github.com/container-storage-interface/spec/lib/go/csi"
log "github.com/sirupsen/logrus"
"github.com/SynologyOpenSource/synology-csi/pkg/interfaces"
"github.com/SynologyOpenSource/synology-csi/pkg/utils"
"github.com/container-storage-interface/spec/lib/go/csi"
log "github.com/sirupsen/logrus"
)
const (
DriverName = "csi.san.synology.com" // CSI dirver name
DriverName = "csi.san.synology.com" // CSI dirver name
DriverVersion = "1.2.0"
)
var (
MultipathEnabled = true
MultipathEnabled = true
supportedProtocolList = []string{utils.ProtocolIscsi, utils.ProtocolSmb, utils.ProtocolNfs}
allowedNfsVersionList = []string{"3", "4", "4.0", "4.1"}
)
@@ -44,13 +44,14 @@ type Driver struct {
nodeID string
version string
endpoint string
tools tools
csCap []*csi.ControllerServiceCapability
vCap []*csi.VolumeCapability_AccessMode
nsCap []*csi.NodeServiceCapability
DsmService interfaces.IDsmService
}
func NewControllerAndNodeDriver(nodeID string, endpoint string, dsmService interfaces.IDsmService) (*Driver, error) {
func NewControllerAndNodeDriver(nodeID string, endpoint string, dsmService interfaces.IDsmService, tools tools) (*Driver, error) {
log.Debugf("NewControllerAndNodeDriver: DriverName: %v, DriverVersion: %v", DriverName, DriverVersion)
// TODO version format and validation
@@ -60,6 +61,7 @@ func NewControllerAndNodeDriver(nodeID string, endpoint string, dsmService inter
nodeID: nodeID,
endpoint: endpoint,
DsmService: dsmService,
tools: tools,
}
d.addControllerServiceCapabilities([]csi.ControllerServiceCapability_RPC_Type{

View File

@@ -24,6 +24,7 @@ import (
"strconv"
"strings"
"github.com/SynologyOpenSource/synology-csi/pkg/utils/hostexec"
log "github.com/sirupsen/logrus"
utilexec "k8s.io/utils/exec"
)
@@ -31,6 +32,7 @@ import (
type initiatorDriver struct {
chapUser string
chapPassword string
tools tools
}
type iscsiSession struct {
@@ -45,10 +47,19 @@ const (
ISCSIPort = 3260
)
func iscsiadm(cmdArgs ...string) utilexec.Cmd {
executor := utilexec.New()
type tools struct {
executor hostexec.Executor
}
return executor.Command("iscsiadm", cmdArgs...)
// NewTools creates a new tools wrapper for calling utilities with given executor
func NewTools(executor hostexec.Executor) tools {
return tools{
executor: executor,
}
}
func (t *tools) iscsiadm(cmdArgs ...string) utilexec.Cmd {
return t.executor.Command("iscsiadm", cmdArgs...)
}
// parseSession takes the raw stdout from the `iscsiadm -m session` command and encodes it into an iSCSI session type
@@ -81,8 +92,8 @@ func parseSessions(lines string) []iscsiSession {
return sessions
}
func iscsiadm_session() []iscsiSession {
cmd := iscsiadm("-m", "session")
func (t *tools) iscsiadm_session() []iscsiSession {
cmd := t.iscsiadm("-m", "session")
out, err := cmd.CombinedOutput()
if err != nil {
exitErr, ok := err.(utilexec.ExitError)
@@ -97,8 +108,8 @@ func iscsiadm_session() []iscsiSession {
return parseSessions(string(out))
}
func iscsiadm_discovery(portal string) error {
cmd := iscsiadm(
func (t *tools) iscsiadm_discovery(portal string) error {
cmd := t.iscsiadm(
"-m", "discoverydb",
"--type", "sendtargets",
"--portal", portal,
@@ -110,8 +121,8 @@ func iscsiadm_discovery(portal string) error {
return nil
}
func iscsiadm_login(iqn, portal string) error {
cmd := iscsiadm(
func (t *tools) iscsiadm_login(iqn, portal string) error {
cmd := t.iscsiadm(
"-m", "node",
"--targetname", iqn,
"--portal", portal,
@@ -123,8 +134,8 @@ func iscsiadm_login(iqn, portal string) error {
return nil
}
func iscsiadm_update_node_startup(iqn, portal string) error {
cmd := iscsiadm(
func (t *tools) iscsiadm_update_node_startup(iqn, portal string) error {
cmd := t.iscsiadm(
"-m", "node",
"--targetname", iqn,
"--portal", portal,
@@ -138,8 +149,8 @@ func iscsiadm_update_node_startup(iqn, portal string) error {
return nil
}
func iscsiadm_logout(iqn string) error {
cmd := iscsiadm(
func (t *tools) iscsiadm_logout(iqn string) error {
cmd := t.iscsiadm(
"-m", "node",
"--targetname", iqn,
"--logout")
@@ -149,8 +160,8 @@ func iscsiadm_logout(iqn string) error {
return nil
}
func iscsiadm_rescan(iqn string) error {
cmd := iscsiadm(
func (t *tools) iscsiadm_rescan(iqn string) error {
cmd := t.iscsiadm(
"-m", "node",
"--targetname", iqn,
"-R")
@@ -160,8 +171,8 @@ func iscsiadm_rescan(iqn string) error {
return nil
}
func hasSession(targetIqn string, portal string) bool {
sessions := iscsiadm_session()
func (t *tools) hasSession(targetIqn string, portal string) bool {
sessions := t.iscsiadm_session()
for _, s := range sessions {
if targetIqn == s.Iqn && (portal == s.Portal || portal == "") {
@@ -172,8 +183,8 @@ func hasSession(targetIqn string, portal string) bool {
return false
}
func listSessionsByIqn(targetIqn string) (matchedSessions []iscsiSession) {
sessions := iscsiadm_session()
func (t *tools) listSessionsByIqn(targetIqn string) (matchedSessions []iscsiSession) {
sessions := t.iscsiadm_session()
for _, s := range sessions {
if targetIqn == s.Iqn {
@@ -185,22 +196,22 @@ func listSessionsByIqn(targetIqn string) (matchedSessions []iscsiSession) {
}
func (d *initiatorDriver) login(targetIqn string, portal string) error {
if hasSession(targetIqn, portal) {
if d.tools.hasSession(targetIqn, portal) {
log.Infof("Session[%s] already exists.", targetIqn)
return nil
}
if err := iscsiadm_discovery(portal); err != nil {
if err := d.tools.iscsiadm_discovery(portal); err != nil {
log.Errorf("Failed in discovery of the target: %v", err)
return err
}
if err := iscsiadm_login(targetIqn, portal); err != nil {
if err := d.tools.iscsiadm_login(targetIqn, portal); err != nil {
log.Errorf("Failed in login of the target: %v", err)
return err
}
if err := iscsiadm_update_node_startup(targetIqn, portal); err != nil {
if err := d.tools.iscsiadm_update_node_startup(targetIqn, portal); err != nil {
log.Warnf("Failed to update target node.startup to manual: %v", err)
}
@@ -210,13 +221,13 @@ func (d *initiatorDriver) login(targetIqn string, portal string) error {
}
func (d *initiatorDriver) logout(targetIqn string, ip string) error {
if !hasSession(targetIqn, "") {
if !d.tools.hasSession(targetIqn, "") {
log.Infof("Session[%s] doesn't exist.", targetIqn)
return nil
}
portal := fmt.Sprintf("%s:%d", ip, ISCSIPort)
if err := iscsiadm_logout(targetIqn); err != nil {
if err := d.tools.iscsiadm_logout(targetIqn); err != nil {
log.Errorf("Failed in logout of the target.\nTarget [%s], Portal [%s], Err[%v]",
targetIqn, portal, err)
return err
@@ -228,13 +239,13 @@ func (d *initiatorDriver) logout(targetIqn string, ip string) error {
}
func (d *initiatorDriver) rescan(targetIqn string) error {
if !hasSession(targetIqn, "") {
if !d.tools.hasSession(targetIqn, "") {
msg := fmt.Sprintf("Session[%s] doesn't exist.", targetIqn)
log.Error(msg)
return errors.New(msg)
}
if err := iscsiadm_rescan(targetIqn); err != nil {
if err := d.tools.iscsiadm_rescan(targetIqn); err != nil {
log.Errorf("Failed in rescan of the target.\nTarget [%s], Err[%v]",
targetIqn, err)
return err

View File

@@ -21,21 +21,23 @@ package driver
import (
"context"
"fmt"
log "github.com/sirupsen/logrus"
"os"
"regexp"
"strings"
"time"
log "github.com/sirupsen/logrus"
utilexec "k8s.io/utils/exec"
)
func IsMultipathEnabled() bool {
return MultipathEnabled && multipathd_is_running()
// IsMultipathEnabled returns true if multipath is enabled
func (t *tools) IsMultipathEnabled() bool {
return MultipathEnabled && t.multipathd_is_running()
}
func multipathd_is_running() bool {
executor := utilexec.New()
cmd := executor.Command("multipathd", "show", "daemon")
func (t *tools) multipathd_is_running() bool {
cmd := t.executor.Command("multipathd", "show", "daemon")
out, err := cmd.CombinedOutput()
if err == nil {
matched, _ := regexp.MatchString(`pid \d+ (running|idle)`, string(out))
@@ -49,15 +51,14 @@ func multipathd_is_running() bool {
}
// execute a command with a timeout and returns an error if timeout is exceeded
func execWithTimeout(command string, args []string, timeout time.Duration) ([]byte, error) {
func (t *tools) execWithTimeout(command string, args []string, timeout time.Duration) ([]byte, error) {
log.Infof("Executing command '%v' with args: '%v'.", command, args)
// Create a new context and add a timeout to it
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
executor := utilexec.New()
cmd := executor.CommandContext(ctx, command, args...)
cmd := t.executor.CommandContext(ctx, command, args...)
out, err := cmd.Output()
log.Debug(err)
@@ -81,9 +82,8 @@ func execWithTimeout(command string, args []string, timeout time.Duration) ([]by
}
// resize a multipath device based on its underlying devices
func multipath_resize(devName string) error {
executor := utilexec.New()
cmd := executor.Command("multipathd", "resize", "map", devName) // use devName not devPath, or it'll fail
func (t *tools) multipath_resize(devName string) error {
cmd := t.executor.Command("multipathd", "resize", "map", devName) // use devName not devPath, or it'll fail
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%s (%v)", string(out), err)
@@ -92,9 +92,9 @@ func multipath_resize(devName string) error {
}
// flushes a multipath device dm-x with command multipath -f /dev/dm-x
func multipath_flush(devPath string) error {
func (t *tools) multipath_flush(devPath string) error {
timeout := 5 * time.Second
out, err := execWithTimeout("multipath", []string{"-f", devPath}, timeout)
out, err := t.execWithTimeout("multipath", []string{"-f", devPath}, timeout)
if err != nil {
if _, e := os.Stat(devPath); os.IsNotExist(e) {
log.Debugf("Multipath device %v has been removed.", devPath)

View File

@@ -28,12 +28,12 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/container-storage-interface/spec/lib/go/csi"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"golang.org/x/sys/unix"
"k8s.io/mount-utils"
clientset "k8s.io/client-go/kubernetes"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/mount-utils"
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/webapi"
"github.com/SynologyOpenSource/synology-csi/pkg/interfaces"
@@ -47,6 +47,7 @@ type nodeServer struct {
dsmService interfaces.IDsmService
Initiator *initiatorDriver
Client clientset.Interface
tools tools
}
func waitForDevicePathToExist(path string) error {
@@ -73,10 +74,10 @@ func waitForDevicePathToExist(path string) error {
}
// for unstage, resize volume
func getExistedVolumeMountPath(targetIqn string, mappingIndex int) string {
func (t *tools) getExistedVolumeMountPath(targetIqn string, mappingIndex int) string {
paths := []string{}
sessions := listSessionsByIqn(targetIqn)
sessions := t.listSessionsByIqn(targetIqn)
for _, session := range sessions {
paths = append(paths, fmt.Sprintf("%sip-%s-iscsi-%s-lun-%d", "/dev/disk/by-path/", session.Portal, targetIqn, mappingIndex))
}
@@ -175,7 +176,7 @@ func (ns *nodeServer) getPortals(dsmIp string) []string {
portals = append(portals, fmt.Sprintf("%s:%d", ips[0], ISCSIPort)) //get the first ip
}
if dsm.IsUC() && IsMultipathEnabled() {
if dsm.IsUC() && ns.tools.IsMultipathEnabled() {
dsm2, err := dsm.GetAnotherController()
if err != nil {
log.Errorf("[%s] UC failed to get another controller: %v", err)
@@ -228,10 +229,10 @@ func (ns *nodeServer) logoutTarget(volumeId string) {
// Assume target and lun 1-1 mapping
mappingIndex := k8sVolume.Target.MappedLuns[0].MappingIndex
volumeMountPath := getExistedVolumeMountPath(k8sVolume.Target.Iqn, mappingIndex)
volumeMountPath := ns.tools.getExistedVolumeMountPath(k8sVolume.Target.Iqn, mappingIndex)
if strings.Contains(volumeMountPath, "/dev/mapper") && IsMultipathEnabled() {
if err := multipath_flush(volumeMountPath); err != nil {
if strings.Contains(volumeMountPath, "/dev/mapper") && ns.tools.IsMultipathEnabled() {
if err := ns.tools.multipath_flush(volumeMountPath); err != nil {
log.Errorf("Failed to remove multipath device in path %s. err: %v", volumeMountPath, err)
}
}
@@ -567,7 +568,7 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis
if req.VolumeContext["protocol"] == utils.ProtocolNfs {
options = append(options, req.GetVolumeCapability().GetMount().GetMountFlags()...)
var server, baseDir string //NFSTODO: subDir
var server, baseDir string //NFSTODO: subDir
var mountPermissionsUint uint64 = 0750 // default
for k, v := range req.GetVolumeContext() {
switch k {
@@ -804,13 +805,13 @@ func (ns *nodeServer) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandV
// Assume target and lun 1-1 mapping
mappingIndex := k8sVolume.Target.MappedLuns[0].MappingIndex
volumeMountPath := getExistedVolumeMountPath(k8sVolume.Target.Iqn, mappingIndex)
volumeMountPath := ns.tools.getExistedVolumeMountPath(k8sVolume.Target.Iqn, mappingIndex)
if volumeMountPath == "" {
return nil, status.Error(codes.Internal, "Can't get volume mount path")
}
if strings.Contains(volumeMountPath, "/dev/mapper") && IsMultipathEnabled() {
if err := multipath_resize(filepath.Base(volumeMountPath)); err != nil {
if strings.Contains(volumeMountPath, "/dev/mapper") && ns.tools.IsMultipathEnabled() {
if err := ns.tools.multipath_resize(filepath.Base(volumeMountPath)); err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to resize multipath device in %s. err: %v", volumeMountPath, err))
}
}

View File

@@ -24,13 +24,13 @@ import (
"strings"
"github.com/container-storage-interface/spec/lib/go/csi"
log "github.com/sirupsen/logrus"
"github.com/kubernetes-csi/csi-lib-utils/protosanitizer"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/utils/exec"
"k8s.io/mount-utils"
"k8s.io/utils/exec"
)
func ParseEndpoint(ep string) (string, string, error) {
@@ -45,7 +45,7 @@ func ParseEndpoint(ep string) (string, string, error) {
func NewControllerServer(d *Driver) *controllerServer {
return &controllerServer{
Driver: d,
Driver: d,
dsmService: d.DsmService,
}
}
@@ -65,17 +65,19 @@ func getK8sClient() clientset.Interface {
func NewNodeServer(d *Driver) *nodeServer {
return &nodeServer{
Driver: d,
Driver: d,
dsmService: d.DsmService,
Mounter: &mount.SafeFormatAndMount{
Interface: mount.New(""),
Exec: exec.New(),
Exec: exec.New(),
},
Initiator: &initiatorDriver{
chapUser: "",
chapUser: "",
chapPassword: "",
tools: d.tools,
},
Client: getK8sClient(),
tools: d.tools,
}
}

View File

@@ -0,0 +1,99 @@
// Package hostexec automatically wraps commands executed with kubernetes hostexec into
// chrooted commands
package hostexec
import (
"context"
"errors"
"fmt"
"os"
"strings"
"k8s.io/utils/exec"
)
// defaultSearchPath for running commands without absolute paths
var defaultSearchPath = []string{
"/usr/local/sbin",
"/usr/local/bin",
"/usr/sbin",
"/usr/bin",
"/sbin",
"/bin",
}
// Executor is mostly k8s.io/utils/exec compatible interface for the portions
// that synology-csi uses.
type Executor interface {
Command(string, ...string) exec.Cmd
CommandContext(context.Context, string, ...string) exec.Cmd
}
type hostexec struct {
Executor
commandMap map[string]string
chrootDir string
}
// New creates an instance of hostexec to execute commands in the given environment
func New(cmdMap map[string]string, chrootDir string) (Executor, error) {
// If chroot directory is defined, check that directory exists or return an error
if chrootDir != "" {
fileinfo, err := os.Stat(chrootDir)
if err != nil || !fileinfo.IsDir() {
return nil, errors.New("chroot directory does not exist or is not a directory")
}
}
return &hostexec{exec.New(), cmdMap, chrootDir}, nil
}
func (h *hostexec) resolveCmd(cmd string, args ...string) (string, []string) {
c, ok := h.commandMap[cmd]
if !ok || c == "" {
return cmd, args
}
return c, args
}
func (h *hostexec) wrapEnv(cmd string, args ...string) (string, []string) {
if strings.ContainsAny(cmd, "/") {
return cmd, args
}
sp := fmt.Sprintf("PATH=%s", strings.Join(defaultSearchPath, ":"))
args = append([]string{"-i", sp, cmd}, args...)
cmd = "/usr/bin/env"
return cmd, args
}
func (h *hostexec) wrapChroot(cmd string, args ...string) (string, []string) {
if h.chrootDir == "" {
return cmd, args
}
args = append([]string{h.chrootDir, cmd}, args...)
cmd = "/usr/sbin/chroot"
return cmd, args
}
func (h *hostexec) wrap(cmd string, args ...string) (string, []string) {
cmd, args = h.resolveCmd(cmd, args...)
cmd, args = h.wrapEnv(cmd, args...)
cmd, args = h.wrapChroot(cmd, args...)
return cmd, args
}
func (h *hostexec) Command(cmd string, args ...string) exec.Cmd {
cmd, args = h.wrap(cmd, args...)
return h.Executor.Command(cmd, args...)
}
func (h *hostexec) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
cmd, args = h.wrap(cmd, args...)
return h.Executor.CommandContext(ctx, cmd, args...)
}

View File

@@ -0,0 +1,393 @@
package hostexec
import (
"context"
"io"
"os"
"reflect"
"testing"
"k8s.io/utils/exec"
)
func TestNew(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "chroot_test")
if err != nil {
t.Fatalf("Temporary directory creation failed: %v", err)
}
defer os.RemoveAll(tmpDir)
tests := []struct {
name string
cmdMap map[string]string
chrootDir string
wantErr bool
}{
{
name: "valid chroot directory",
cmdMap: nil,
chrootDir: tmpDir,
wantErr: false,
},
{
name: "invalid chroot directory",
cmdMap: nil,
chrootDir: "/invalid/path",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := New(tt.cmdMap, tt.chrootDir)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestHostexec_wrapEnv(t *testing.T) {
tests := []struct {
name string
cmd string
args []string
wantCmd string
wantArgs []string
}{
{
name: "empty args",
cmd: "echo",
args: nil,
wantCmd: "/usr/bin/env",
wantArgs: []string{"-i", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "echo"},
},
{
name: "non-empty args",
cmd: "echo",
args: []string{"hello", "world"},
wantCmd: "/usr/bin/env",
wantArgs: []string{"-i", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "echo", "hello", "world"},
},
{
name: "fully qualified cmd without args",
cmd: "/bin/echo",
args: nil,
wantCmd: "/bin/echo",
wantArgs: nil,
},
{
name: "fully qualified cmd with args",
cmd: "/bin/echo",
args: []string{"hello", "world"},
wantCmd: "/bin/echo",
wantArgs: []string{"hello", "world"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &hostexec{}
c, a := h.wrapEnv(tt.cmd, tt.args...)
if c != tt.wantCmd {
t.Errorf("wrapEnv() c = %v, want %v", c, tt.wantCmd)
}
if !reflect.DeepEqual(a, tt.wantArgs) {
t.Errorf("wrapEnv() a = %v, want %v", a, tt.wantArgs)
}
})
}
}
func TestHostexec_resolveCmd(t *testing.T) {
tests := []struct {
name string
cmd string
cmdArgs []string
cmdMap map[string]string
wantCmd string
wantCmdArgs []string
}{
{
name: "empty cmd map",
cmd: "echo",
cmdArgs: []string{"hello", "world"},
cmdMap: nil,
wantCmd: "echo",
wantCmdArgs: []string{"hello", "world"},
},
{
name: "non-empty cmd map",
cmd: "echo",
cmdArgs: []string{"hello", "world"},
cmdMap: map[string]string{
"echo": "/bin/echo",
},
wantCmd: "/bin/echo",
wantCmdArgs: []string{"hello", "world"},
},
{
name: "non-empty cmd map without matching command",
cmd: "echo",
cmdArgs: []string{"hello", "world"},
cmdMap: map[string]string{
"cat": "/dummy/cat",
},
wantCmd: "echo",
wantCmdArgs: []string{"hello", "world"},
},
{
name: "empty string mapping",
cmd: "echo",
cmdArgs: []string{"hello", "world"},
cmdMap: map[string]string{
"echo": "",
},
wantCmd: "echo",
wantCmdArgs: []string{"hello", "world"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &hostexec{commandMap: tt.cmdMap}
c, a := h.resolveCmd(tt.cmd, tt.cmdArgs...)
if c != tt.wantCmd {
t.Errorf("resolveCmd() c = %v, want %v", c, tt.wantCmd)
}
if !reflect.DeepEqual(a, tt.wantCmdArgs) {
t.Errorf("resolveCmd() a = %v, want %v", a, tt.wantCmdArgs)
}
})
}
}
func TestHostexec_wrapChroot(t *testing.T) {
tests := []struct {
name string
chroot string
cmd string
args []string
wantCmd string
wantArgs []string
}{
{
name: "empty chroot",
chroot: "",
cmd: "echo",
args: []string{"hello", "world"},
wantCmd: "echo",
wantArgs: []string{"hello", "world"},
},
{
name: "non-empty chroot",
chroot: "/tmp",
cmd: "echo",
args: []string{"hello", "world"},
wantCmd: "/usr/sbin/chroot",
wantArgs: []string{"/tmp", "echo", "hello", "world"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &hostexec{chrootDir: tt.chroot}
c, a := h.wrapChroot(tt.cmd, tt.args...)
if c != tt.wantCmd {
t.Errorf("wrapChroot() c = %v, want %v", c, tt.wantCmd)
}
if !reflect.DeepEqual(a, tt.wantArgs) {
t.Errorf("wrapChroot() a = %v, want %v", a, tt.wantArgs)
}
})
}
}
func TestHostexec_wrap(t *testing.T) {
tests := []struct {
name string
cmd string
args []string
chroot string
cmdMap map[string]string
wantCmd string
wantArgs []string
}{
{
name: "no wrappers",
cmd: "/bin/echo",
args: []string{
"hello",
"world",
},
chroot: "",
cmdMap: nil,
wantCmd: "/bin/echo",
wantArgs: []string{
"hello",
"world",
},
},
{
name: "env wrapper",
cmd: "echo",
args: []string{
"hello",
"world",
},
chroot: "",
cmdMap: nil,
wantCmd: "/usr/bin/env",
wantArgs: []string{
"-i", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "echo", "hello",
"world",
},
},
{
name: "chroot wrapper",
cmd: "/bin/echo",
args: []string{"hello", "world"},
chroot: "/tmp",
cmdMap: nil,
wantCmd: "/usr/sbin/chroot",
wantArgs: []string{
"/tmp",
"/bin/echo",
"hello",
"world",
},
},
{
name: "cmd path wrapper",
cmd: "echo",
args: []string{
"hello",
"world",
},
chroot: "",
cmdMap: map[string]string{
"echo": "/bin/echo",
},
wantCmd: "/bin/echo",
wantArgs: []string{
"hello", "world",
},
},
{
name: "chain wrappers",
cmd: "echo",
args: []string{"hello", "world"},
chroot: "/tmp",
cmdMap: map[string]string{"echo": "/bin/echo"},
wantCmd: "/usr/sbin/chroot",
wantArgs: []string{
"/tmp",
"/bin/echo",
"hello", "world",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &hostexec{
commandMap: tt.cmdMap,
chrootDir: tt.chroot,
}
c, a := h.wrap(tt.cmd, tt.args...)
if c != tt.wantCmd {
t.Errorf("wrap() c = %v, want %v", c, tt.wantCmd)
}
if !reflect.DeepEqual(a, tt.wantArgs) {
t.Errorf("wrap() a = %v, want %v", a, tt.wantArgs)
}
})
}
}
// dummyCmd statisfies the interface for exec.Cmd for testing purposes
type dummyCmd struct{}
func (dummyCmd) Run() error { return nil }
func (dummyCmd) CombinedOutput() ([]byte, error) { return nil, nil }
func (dummyCmd) Output() ([]byte, error) { return nil, nil }
func (dummyCmd) SetDir(string) {}
func (dummyCmd) SetStdin(io.Reader) {}
func (dummyCmd) SetStdout(io.Writer) {}
func (dummyCmd) SetStderr(io.Writer) {}
func (dummyCmd) SetEnv([]string) {}
func (dummyCmd) StdoutPipe() (io.ReadCloser, error) { return nil, nil }
func (dummyCmd) StderrPipe() (io.ReadCloser, error) { return nil, nil }
func (dummyCmd) Start() error { return nil }
func (dummyCmd) Wait() error { return nil }
func (dummyCmd) Stop() {}
// dummyInterface satisfies the interface for exec.Interface for testing purposes
type dummyInterface struct {
commandFunc func(string, ...string) exec.Cmd
commandContextFunc func(context.Context, string, ...string) exec.Cmd
lookPathFunc func(string) (string, error)
}
func (d *dummyInterface) Command(cmd string, args ...string) exec.Cmd {
return d.commandFunc(cmd, args...)
}
func (d *dummyInterface) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
return d.commandContextFunc(ctx, cmd, args...)
}
func (d *dummyInterface) LookPath(path string) (string, error) { return d.lookPathFunc(path) }
func TestHostexec_Command(t *testing.T) {
successCmd := dummyCmd{}
i := &dummyInterface{
commandFunc: func(cmd string, args ...string) exec.Cmd {
if cmd != "/bin/echo" {
t.Errorf("Command() cmd = %v, want %v", cmd, "echo")
}
if !reflect.DeepEqual(args, []string{"hello", "world"}) {
t.Errorf("Command() args = %v, want %v", args, []string{"hello", "world"})
}
return successCmd
},
}
h := &hostexec{
Executor: i,
commandMap: map[string]string{
"echo": "/bin/echo",
},
}
c := h.Command("echo", "hello", "world")
if c != successCmd {
t.Errorf("Command() c = %v, want %v", c, successCmd)
}
}
func TestHostexec_CommandContext(t *testing.T) {
successCmd := dummyCmd{}
i := &dummyInterface{
commandContextFunc: func(_ context.Context, cmd string, args ...string) exec.Cmd {
if cmd != "/bin/echo" {
t.Errorf("Command() cmd = %v, want %v", cmd, "echo")
}
if !reflect.DeepEqual(args, []string{"hello", "world"}) {
t.Errorf("Command() args = %v, want %v", args, []string{"hello", "world"})
}
return successCmd
},
}
h := &hostexec{
Executor: i,
commandMap: map[string]string{
"echo": "/bin/echo",
},
}
c := h.CommandContext(context.Background(), "echo", "hello", "world")
if c != successCmd {
t.Errorf("Command() c = %v, want %v", c, successCmd)
}
}

View File

@@ -11,10 +11,11 @@ import (
"github.com/SynologyOpenSource/synology-csi/pkg/driver"
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/common"
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/service"
"github.com/SynologyOpenSource/synology-csi/pkg/utils/hostexec"
)
const (
ConfigPath = "./../../config/client-info.yml"
ConfigPath = "./../../config/client-info.yml"
SecretsFilePath = "./sanity-test-secret-file.yaml"
)
@@ -58,8 +59,14 @@ func TestSanity(t *testing.T) {
}
defer dsmService.RemoveAllDsms()
cmdExecutor, err := hostexec.New(nil, "")
if err != nil {
t.Fatal(fmt.Sprintf("Failed to create command executor: %v\n", err))
}
tools := driver.NewTools(cmdExecutor)
endpoint := "unix://" + endpointFile.Name()
drv, err := driver.NewControllerAndNodeDriver(nodeID, endpoint, dsmService)
drv, err := driver.NewControllerAndNodeDriver(nodeID, endpoint, dsmService, tools)
if err != nil {
t.Fatal(fmt.Sprintf("Failed to create driver: %v\n", err))
}