Skip to content

Commit 8677df5

Browse files
authored
Create runner.RunInForeground function in github.com/awslabs/aws-s3-csi-driver/pkg/mountpoint/runner package (#448)
This is part of the effort of consolidating Mountpoint related functionalities in `github.com/awslabs/aws-s3-csi-driver/pkg/mountpoint` package. --- By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. Signed-off-by: Burak Varlı <burakvar@amazon.co.uk>
1 parent 0f6b656 commit 8677df5

4 files changed

Lines changed: 135 additions & 61 deletions

File tree

cmd/aws-s3-csi-mounter/csimounter/csimounter.go

Lines changed: 16 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
package csimounter
22

33
import (
4-
"bytes"
54
"fmt"
6-
"io"
75
"io/fs"
86
"os"
9-
"os/exec"
107

118
"k8s.io/klog/v2"
129

1310
"github.com/awslabs/aws-s3-csi-driver/pkg/mountpoint"
1411
"github.com/awslabs/aws-s3-csi-driver/pkg/mountpoint/mountoptions"
12+
"github.com/awslabs/aws-s3-csi-driver/pkg/mountpoint/runner"
1513
)
1614

1715
var mountErrorFileperm = fs.FileMode(0600) // only owner readable and writeable
@@ -20,75 +18,43 @@ var mountErrorFileperm = fs.FileMode(0600) // only owner readable and writeable
2018
// so Kubernetes doesn't have to restart it and transition the Pod into `Succeeded` state.
2119
const successExitCode = 0
2220

23-
// A CmdRunner is responsible for running given `cmd` until completion and returning its exit code and its error (if any).
24-
// This is mainly exposed for mocking in tests, `DefaultCmdRunner` is always used in non-test environments.
25-
type CmdRunner func(cmd *exec.Cmd) (int, error)
26-
27-
// DefaultCmdRunner is a real CmdRunner implementation that runs given `cmd`.
28-
func DefaultCmdRunner(cmd *exec.Cmd) (int, error) {
29-
err := cmd.Run()
30-
if err != nil {
31-
return 0, err
32-
}
33-
return cmd.ProcessState.ExitCode(), nil
34-
}
21+
// restartExitCode is the exit code returned from `aws-s3-csi-mounter` to indicate an error exit,
22+
// so Kubernetes would restart it. This is the default exit code if `mount.exit` is not present,
23+
// meaning the CSI Driver Node Pod didn't requested a clean exit.
24+
const restartExitCode = 1
3525

3626
// An Options represents options to use while mounting Mountpoint.
3727
type Options struct {
3828
MountpointPath string
3929
MountExitPath string
4030
MountErrPath string
4131
MountOptions mountoptions.Options
42-
CmdRunner CmdRunner
32+
CmdRunner runner.CmdRunner
4333
}
4434

4535
// Run runs Mountpoint with given options until completion and returns its exit code and its error (if any).
4636
func Run(options Options) (int, error) {
47-
if options.CmdRunner == nil {
48-
options.CmdRunner = DefaultCmdRunner
49-
}
50-
5137
mountOptions := options.MountOptions
52-
53-
fuseDev := os.NewFile(uintptr(mountOptions.Fd), "/dev/fuse")
54-
if fuseDev == nil {
55-
return 0, fmt.Errorf("passed file descriptor %d is invalid", mountOptions.Fd)
56-
}
57-
5838
mountpointArgs := mountpoint.ParseArgs(mountOptions.Args)
5939

60-
// By default Mountpoint runs in a detached mode. Here we want to monitor it by relaying its output,
61-
// and also we want to wait until it terminates. We're passing `--foreground` to achieve this.
62-
mountpointArgs.Set(mountpoint.ArgForeground, mountpoint.ArgNoValue)
63-
6440
// TODO: This is a temporary workaround to create a cache folder if caching is enabled,
6541
// ideally we should create a volume (`emptyDir` by default) in the Mountpoint Pod and use that.
6642
mountpointArgs, err := createCacheDir(mountpointArgs)
6743
if err != nil {
6844
return 0, fmt.Errorf("failed to create cache dir: %w", err)
6945
}
7046

71-
args := append([]string{
72-
mountOptions.BucketName,
73-
// We pass FUSE fd using `ExtraFiles`, and each entry becomes as file descriptor 3+i.
74-
"/dev/fd/3",
75-
}, mountpointArgs.SortedList()...)
76-
77-
cmd := exec.Command(options.MountpointPath, args...)
78-
cmd.ExtraFiles = []*os.File{fuseDev}
79-
cmd.Env = options.MountOptions.Env
80-
81-
var stderrBuf bytes.Buffer
82-
83-
// Connect Mountpoint's stdout/stderr to this commands stdout/stderr,
84-
// so Mountpoint logs can be viewable with `kubectl logs`.
85-
cmd.Stdout = os.Stdout
86-
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
87-
88-
exitCode, err := options.CmdRunner(cmd)
47+
exitCode, stdErr, err := runner.RunInForeground(runner.ForegroundOptions{
48+
BinaryPath: options.MountpointPath,
49+
BucketName: mountOptions.BucketName,
50+
Fd: mountOptions.Fd,
51+
Args: mountpointArgs,
52+
Env: mountOptions.Env,
53+
CmdRunner: options.CmdRunner,
54+
})
8955
if err != nil {
9056
// If Mountpoint fails, write it to `options.MountErrPath` to let `PodMounter` running in the same node know.
91-
if writeErr := os.WriteFile(options.MountErrPath, stderrBuf.Bytes(), mountErrorFileperm); writeErr != nil {
57+
if writeErr := os.WriteFile(options.MountErrPath, stdErr, mountErrorFileperm); writeErr != nil {
9258
klog.Errorf("Failed to write mount error logs to %s: %v\n", options.MountErrPath, err)
9359
}
9460
return exitCode, err
@@ -100,7 +66,7 @@ func Run(options Options) (int, error) {
10066
return successExitCode, nil
10167
}
10268

103-
return exitCode, nil
69+
return restartExitCode, nil
10470
}
10571

10672
// checkIfFileExists checks whether given `path` exists.

cmd/aws-s3-csi-mounter/csimounter/csimounter_test.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import (
1515
"github.com/awslabs/aws-s3-csi-driver/pkg/util/testutil/assert"
1616
)
1717

18+
const successExitCode = 0
19+
const restartExitCode = 1
20+
1821
func TestRunningMountpoint(t *testing.T) {
1922
mountpointPath := filepath.Join(t.TempDir(), "mount-s3")
2023

@@ -37,7 +40,7 @@ func TestRunningMountpoint(t *testing.T) {
3740
CmdRunner: runner,
3841
})
3942
assert.NoError(t, err)
40-
assert.Equals(t, 0, exitCode)
43+
assert.Equals(t, restartExitCode, exitCode)
4144
})
4245

4346
t.Run("Passes bucket name", func(t *testing.T) {
@@ -56,7 +59,7 @@ func TestRunningMountpoint(t *testing.T) {
5659
CmdRunner: runner,
5760
})
5861
assert.NoError(t, err)
59-
assert.Equals(t, 0, exitCode)
62+
assert.Equals(t, restartExitCode, exitCode)
6063
})
6164

6265
t.Run("Passes environment variables", func(t *testing.T) {
@@ -70,13 +73,14 @@ func TestRunningMountpoint(t *testing.T) {
7073
exitCode, err := csimounter.Run(csimounter.Options{
7174
MountpointPath: mountpointPath,
7275
MountOptions: mountoptions.Options{
73-
Fd: int(mountertest.OpenDevNull(t).Fd()),
74-
Env: env,
76+
Fd: int(mountertest.OpenDevNull(t).Fd()),
77+
BucketName: "test-bucket",
78+
Env: env,
7579
},
7680
CmdRunner: runner,
7781
})
7882
assert.NoError(t, err)
79-
assert.Equals(t, 0, exitCode)
83+
assert.Equals(t, restartExitCode, exitCode)
8084
})
8185

8286
t.Run("Adds `--foreground` argument if not passed", func(t *testing.T) {
@@ -98,7 +102,7 @@ func TestRunningMountpoint(t *testing.T) {
98102
CmdRunner: runner,
99103
})
100104
assert.NoError(t, err)
101-
assert.Equals(t, 0, exitCode)
105+
assert.Equals(t, restartExitCode, exitCode)
102106

103107
exitCode, err = csimounter.Run(csimounter.Options{
104108
MountpointPath: mountpointPath,
@@ -110,7 +114,7 @@ func TestRunningMountpoint(t *testing.T) {
110114
CmdRunner: runner,
111115
})
112116
assert.NoError(t, err)
113-
assert.Equals(t, 0, exitCode)
117+
assert.Equals(t, restartExitCode, exitCode)
114118
})
115119

116120
t.Run("Fails if file descriptor is invalid", func(t *testing.T) {
@@ -139,7 +143,7 @@ func TestRunningMountpoint(t *testing.T) {
139143
_, err := c.Stderr.Write([]byte(mountpointErr.Error()))
140144
assert.NoError(t, err)
141145

142-
return 1, mountpointErr
146+
return restartExitCode, mountpointErr
143147
}
144148

145149
exitCode, err := csimounter.Run(csimounter.Options{
@@ -152,7 +156,7 @@ func TestRunningMountpoint(t *testing.T) {
152156
CmdRunner: runner,
153157
})
154158
assert.Equals(t, mountpointErr, err)
155-
assert.Equals(t, 1, exitCode)
159+
assert.Equals(t, restartExitCode, exitCode)
156160

157161
errMsg, err := os.ReadFile(mountErrPath)
158162
assert.NoError(t, err)
@@ -185,7 +189,7 @@ func TestRunningMountpoint(t *testing.T) {
185189
CmdRunner: runner,
186190
})
187191
assert.NoError(t, err)
188-
// Should be `0` even though Mountpoint exited with a different exit code
189-
assert.Equals(t, 0, exitCode)
192+
// Should be `successExitCode` even though Mountpoint exited with a different exit code
193+
assert.Equals(t, successExitCode, exitCode)
190194
})
191195
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package runner
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"os"
9+
"os/exec"
10+
11+
"github.com/awslabs/aws-s3-csi-driver/pkg/mountpoint"
12+
)
13+
14+
// ErrMissingBinaryPath is returned when Mountpoint binary path is empty.
15+
var ErrMissingBinaryPath = errors.New("runner: missing Mountpoint binary path")
16+
17+
// ErrMissingBucketName is returned when S3 Bucket name is empty.
18+
var ErrMissingBucketName = errors.New("runner: missing S3 Bucket name")
19+
20+
// A ForegroundOptions represents options for running Mountpoint in the foreground.
21+
type ForegroundOptions struct {
22+
// Path to the Mountpoint binary `mount-s3`.
23+
BinaryPath string
24+
// Name of the S3 Bucket to mount.
25+
BucketName string
26+
// FUSE file descriptor for Mountpoint process to communicate with the kernel.
27+
// Can be obtained using `github.com/awslabs/aws-s3-csi-driver/pkg/mountpoint/mounter` package.
28+
Fd int
29+
// Mountpoint arguments.
30+
Args mountpoint.Args
31+
// Mountpoint processes's environment variables.
32+
Env []string
33+
// Command runner to use, if nil, [DefaultCmdRunner] will be used.
34+
CmdRunner CmdRunner
35+
}
36+
37+
// RunInForeground runs Mountpoint in the foreground until completion.
38+
// It returns Mountpoint processes's exit code, standard error output (if Mountpunt ran and fail), and any error occurred.
39+
// It redirects Mountpoint processes's stdout and stderr to calling process.
40+
func RunInForeground(opts ForegroundOptions) (ExitCode, []byte, error) {
41+
if opts.BinaryPath == "" {
42+
return 0, nil, ErrMissingBinaryPath
43+
}
44+
if opts.BucketName == "" {
45+
return 0, nil, ErrMissingBucketName
46+
}
47+
if opts.CmdRunner == nil {
48+
opts.CmdRunner = DefaultCmdRunner
49+
}
50+
51+
fuseDev := os.NewFile(uintptr(opts.Fd), "/dev/fuse")
52+
if fuseDev == nil {
53+
return 0, nil, fmt.Errorf("runner: passed file descriptor %d is not a valid FUSE file descriptor", opts.Fd)
54+
}
55+
56+
mountpointArgs := opts.Args
57+
58+
// By default Mountpoint runs in a detached mode. Here we want to monitor it by relaying its output,
59+
// and also we want to wait until it terminates. We're passing `--foreground` to achieve this.
60+
mountpointArgs.Set(mountpoint.ArgForeground, mountpoint.ArgNoValue)
61+
62+
args := append([]string{
63+
opts.BucketName,
64+
// We pass FUSE fd using `ExtraFiles`, and each entry becomes as file descriptor 3+i.
65+
"/dev/fd/3",
66+
}, mountpointArgs.SortedList()...)
67+
68+
cmd := exec.Command(opts.BinaryPath, args...)
69+
cmd.ExtraFiles = []*os.File{fuseDev}
70+
cmd.Env = opts.Env
71+
72+
var stderrBuf bytes.Buffer
73+
// Connect Mountpoint's stdout/stderr to this commands stdout/stderr,
74+
// as we're running Mountpoint in the foreground.
75+
cmd.Stdout = os.Stdout
76+
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
77+
78+
exitCode, err := opts.CmdRunner(cmd)
79+
if err != nil {
80+
return exitCode, stderrBuf.Bytes(), err
81+
}
82+
83+
return exitCode, nil, nil
84+
}

pkg/mountpoint/runner/runner.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Package runner provides utilities for running Mountpoint instances.
2+
package runner
3+
4+
import "os/exec"
5+
6+
// An ExitCode represents exit code of a Mountpoint process.
7+
type ExitCode = int
8+
9+
// A CmdRunner is responsible for running given `cmd` until completion and returning its exit code and its error (if any).
10+
// This is mainly exposed for mocking in tests, [DefaultCmdRunner] is always used in non-test environments.
11+
type CmdRunner func(cmd *exec.Cmd) (ExitCode, error)
12+
13+
// DefaultCmdRunner is a real CmdRunner implementation that runs given `cmd`.
14+
func DefaultCmdRunner(cmd *exec.Cmd) (ExitCode, error) {
15+
err := cmd.Run()
16+
if err != nil {
17+
return 0, err
18+
}
19+
return cmd.ProcessState.ExitCode(), nil
20+
}

0 commit comments

Comments
 (0)