Skip to content

Commit 9ee20e7

Browse files
committed
fix: add preExec to Run (#368)
Fixes #364 Adds the capability for Run() to run defers defined outside of Run(). Currently the only use-case for this is to close the Coder logger. Also adds an integration test that validates this behaviour and ensures that closeLogs gets called. Co-authored-by: Mathias Fredriksson <[email protected]> (cherry picked from commit 481e3a9)
1 parent 7955333 commit 9ee20e7

File tree

3 files changed

+84
-2
lines changed

3 files changed

+84
-2
lines changed

cmd/envbuilder/main.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ func envbuilderCmd() serpent.Command {
3737
Options: o.CLI(),
3838
Handler: func(inv *serpent.Invocation) error {
3939
o.SetDefaults()
40+
var preExec []func()
41+
defer func() { // Ensure cleanup in case of error.
42+
for _, fn := range preExec {
43+
fn()
44+
}
45+
}()
4046
o.Logger = log.New(os.Stderr, o.Verbose)
4147
if o.CoderAgentURL != "" {
4248
if o.CoderAgentToken == "" {
@@ -50,6 +56,10 @@ func envbuilderCmd() serpent.Command {
5056
if err == nil {
5157
o.Logger = log.Wrap(o.Logger, coderLog)
5258
defer closeLogs()
59+
preExec = append(preExec, func() {
60+
o.Logger(log.LevelInfo, "Closing logs")
61+
closeLogs()
62+
})
5363
// This adds the envbuilder subsystem.
5464
// If telemetry is enabled in a Coder deployment,
5565
// this will be reported and help us understand
@@ -78,7 +88,7 @@ func envbuilderCmd() serpent.Command {
7888
return nil
7989
}
8090

81-
err := envbuilder.Run(inv.Context(), o)
91+
err := envbuilder.Run(inv.Context(), o, preExec...)
8292
if err != nil {
8393
o.Logger(log.LevelError, "error: %s", err)
8494
}

envbuilder.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ type execArgsInfo struct {
8484
// Logger is the logf to use for all operations.
8585
// Filesystem is the filesystem to use for all operations.
8686
// Defaults to the host filesystem.
87-
func Run(ctx context.Context, opts options.Options) error {
87+
// preExec are any functions that should be called before exec'ing the init
88+
// command. This is useful for ensuring that defers get run.
89+
func Run(ctx context.Context, opts options.Options, preExec ...func()) error {
8890
var args execArgsInfo
8991
// Run in a separate function to ensure all defers run before we
9092
// setuid or exec.
@@ -103,6 +105,9 @@ func Run(ctx context.Context, opts options.Options) error {
103105
}
104106

105107
opts.Logger(log.LevelInfo, "=== Running the init command %s %+v as the %q user...", opts.InitCommand, args.InitArgs, args.UserInfo.user.Username)
108+
for _, fn := range preExec {
109+
fn()
110+
}
106111

107112
err = syscall.Exec(args.InitCommand, append([]string{args.InitCommand}, args.InitArgs...), args.Environ)
108113
if err != nil {

integration/integration_test.go

+67
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"testing"
2424
"time"
2525

26+
"github.com/coder/coder/v2/codersdk"
27+
"github.com/coder/coder/v2/codersdk/agentsdk"
2628
"github.com/coder/envbuilder"
2729
"github.com/coder/envbuilder/devcontainer/features"
2830
"github.com/coder/envbuilder/internal/magicdir"
@@ -58,6 +60,71 @@ const (
5860
testImageUbuntu = "localhost:5000/envbuilder-test-ubuntu:latest"
5961
)
6062

63+
func TestLogs(t *testing.T) {
64+
t.Parallel()
65+
66+
token := uuid.NewString()
67+
logsDone := make(chan struct{})
68+
69+
logHandler := func(w http.ResponseWriter, r *http.Request) {
70+
switch r.URL.Path {
71+
case "/api/v2/buildinfo":
72+
w.Header().Set("Content-Type", "application/json")
73+
_, _ = w.Write([]byte(`{"version": "v2.8.9"}`))
74+
return
75+
case "/api/v2/workspaceagents/me/logs":
76+
w.WriteHeader(http.StatusOK)
77+
tokHdr := r.Header.Get(codersdk.SessionTokenHeader)
78+
assert.Equal(t, token, tokHdr)
79+
var req agentsdk.PatchLogs
80+
err := json.NewDecoder(r.Body).Decode(&req)
81+
if err != nil {
82+
http.Error(w, err.Error(), http.StatusBadRequest)
83+
return
84+
}
85+
for _, log := range req.Logs {
86+
t.Logf("got log: %+v", log)
87+
if strings.Contains(log.Output, "Closing logs") {
88+
close(logsDone)
89+
return
90+
}
91+
}
92+
return
93+
default:
94+
t.Errorf("unexpected request to %s", r.URL.Path)
95+
w.WriteHeader(http.StatusNotFound)
96+
return
97+
}
98+
}
99+
logSrv := httptest.NewServer(http.HandlerFunc(logHandler))
100+
defer logSrv.Close()
101+
102+
// Ensures that a Git repository with a devcontainer.json is cloned and built.
103+
srv := gittest.CreateGitServer(t, gittest.Options{
104+
Files: map[string]string{
105+
"devcontainer.json": `{
106+
"build": {
107+
"dockerfile": "Dockerfile"
108+
},
109+
}`,
110+
"Dockerfile": fmt.Sprintf(`FROM %s`, testImageUbuntu),
111+
},
112+
})
113+
_, err := runEnvbuilder(t, runOpts{env: []string{
114+
envbuilderEnv("GIT_URL", srv.URL),
115+
"CODER_AGENT_URL=" + logSrv.URL,
116+
"CODER_AGENT_TOKEN=" + token,
117+
}})
118+
require.NoError(t, err)
119+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
120+
defer cancel()
121+
select {
122+
case <-ctx.Done():
123+
t.Fatal("timed out waiting for logs")
124+
case <-logsDone:
125+
}
126+
}
127+
61128
func TestInitScriptInitCommand(t *testing.T) {
62129
t.Parallel()
63130

0 commit comments

Comments
 (0)