Skip to content

Commit 9f7c3a3

Browse files
[ONPREM-2408] Add entrypoint override (#174)
* Add entrypoint override * Add changelog entry * Make entrypoint override a command * Fix entrypoint args * Add acceptance test * Skip acceptance test on Windows * Add better help description for entrypoint override
1 parent 5d02391 commit 9f7c3a3

File tree

10 files changed

+190
-48
lines changed

10 files changed

+190
-48
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ By following these guidelines, we can easily determine which changes should be i
1010

1111
## Edge
1212

13+
- [#174](https://github.com/circleci/runner-init/pull/174) Add support for a custom entrypoint override.
1314
- [#224](https://github.com/circleci/runner-init/pull/224) Record and log timings in orchestrator init function.
1415
- [#212](https://github.com/circleci/runner-init/pull/212) Fix child process cleanup on Windows using job objects. This ensures that child processes are destroyed when the parent process (task-agent) terminates.
1516
- [#197](https://github.com/circleci/runner-init/pull/197) Fix `%PATH%` on Windows by using the OS-specific path list separator.

acceptance/acceptance_test.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import (
1212
)
1313

1414
var (
15-
orchestratorTestBinary = os.Getenv("ORCHESTRATOR_TEST_BINARY")
16-
orchestratorTestBinaryRunTask = ""
15+
orchestratorTestBinary = os.Getenv("ORCHESTRATOR_TEST_BINARY")
16+
orchestratorTestBinaryRunTask = ""
17+
orchestratorTestBinaryOverride = ""
1718

1819
binariesPath = ""
1920
taskAgentBinary = ""
@@ -60,32 +61,35 @@ func runTests(m *testing.M) (int, error) {
6061
fmt.Printf("Using 'orchestrator' test binary: %q\n", orchestratorTestBinary)
6162
fmt.Printf("Using fake 'task-agent' test binary: %q\n", taskAgentBinary)
6263

63-
if err := createRunTaskScript(); err != nil {
64+
if orchestratorTestBinaryRunTask, err = createRunnerScript("run-task"); err != nil {
6465
return 0, err
6566
}
6667
fmt.Printf("Using 'orchestrator run-task' script: %q\n", orchestratorTestBinaryRunTask)
6768

69+
if orchestratorTestBinaryOverride, err = createRunnerScript("override"); err != nil {
70+
return 0, err
71+
}
72+
fmt.Printf("Using 'orchestrator override' script: %q\n", orchestratorTestBinaryOverride)
73+
6874
return m.Run(), nil
6975
}
7076

7177
// A little hack to get around limitations of the test runner on positional arguments
72-
func createRunTaskScript() error {
78+
func createRunnerScript(cmd string) (string, error) {
7379
var script string
7480
var scriptPath string
7581

7682
if runtime.GOOS == "windows" {
77-
script = "@echo off\n" + orchestratorTestBinary + " run-task"
78-
scriptPath = filepath.Join(binariesPath, "orchestratorRunTask.bat")
83+
script = "@echo off\n" + orchestratorTestBinary + " " + cmd
84+
scriptPath = filepath.Join(binariesPath, fmt.Sprintf("orchestrator-%s.bat", cmd))
7985
} else {
80-
script = "#!/bin/bash\nexec " + orchestratorTestBinary + " run-task"
81-
scriptPath = filepath.Join(binariesPath, "orchestratorRunTask.sh")
86+
script = "#!/bin/bash\nexec " + orchestratorTestBinary + " " + cmd
87+
scriptPath = filepath.Join(binariesPath, fmt.Sprintf("orchestrator-%s.sh", cmd))
8288
}
8389

8490
if err := os.WriteFile(scriptPath, []byte(script), 0750); err != nil { //nolint:gosec
85-
return err
91+
return "", err
8692
}
8793

88-
orchestratorTestBinaryRunTask = scriptPath
89-
90-
return nil
94+
return scriptPath, nil
9195
}

acceptance/task_test.go

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"runtime"
78
"strings"
89
"testing"
910
"time"
1011

1112
"github.com/circleci/ex/testing/runner"
1213
"gotest.tools/v3/assert"
14+
"gotest.tools/v3/assert/cmp"
1315
)
1416

1517
func TestRunTask(t *testing.T) {
@@ -26,32 +28,81 @@ func TestRunTask(t *testing.T) {
2628
"max_run_time": 60000000000
2729
}`, strings.ReplaceAll(readinessFilePath, `\`, `\\`), strings.ReplaceAll(taskAgentBinary, `\`, `\\`))
2830

29-
r := runner.New(
30-
"CIRCLECI_GOAT_SHUTDOWN_DELAY=10s",
31-
"CIRCLECI_GOAT_CONFIG="+goodConfig,
32-
"CIRCLECI_GOAT_HEALTH_CHECK_ADDR=:7624",
33-
)
34-
res, err := r.Start(orchestratorTestBinaryRunTask)
35-
assert.NilError(t, err)
31+
t.Run("Good run-task", func(t *testing.T) {
32+
r := runner.New(
33+
"CIRCLECI_GOAT_SHUTDOWN_DELAY=10s",
34+
"CIRCLECI_GOAT_CONFIG="+goodConfig,
35+
"CIRCLECI_GOAT_HEALTH_CHECK_ADDR=:7624",
36+
)
37+
res, err := r.Start(orchestratorTestBinaryRunTask)
38+
assert.NilError(t, err)
39+
40+
t.Run("Probe for readiness", func(t *testing.T) {
41+
assert.NilError(t, res.Ready("admin", time.Second*20))
42+
})
43+
44+
go func() {
45+
f, err := os.Create(readinessFilePath) //nolint:gosec
46+
defer func() { assert.NilError(t, f.Close()) }()
47+
assert.NilError(t, err)
48+
}()
3649

37-
t.Run("Probe for readiness", func(t *testing.T) {
38-
assert.NilError(t, res.Ready("admin", time.Second*20))
50+
t.Run("Run task", func(t *testing.T) {
51+
select {
52+
case err = <-res.Wait():
53+
assert.NilError(t, err)
54+
case <-time.After(time.Second * 40):
55+
assert.NilError(t, res.Stop())
56+
t.Fatal(t, "timeout before process stopped")
57+
}
58+
})
3959
})
4060

41-
go func() {
42-
f, err := os.Create(readinessFilePath) //nolint:gosec
43-
defer func() { assert.NilError(t, f.Close()) }()
61+
t.Run("Good entrypoint override", func(t *testing.T) {
62+
if runtime.GOOS == "windows" {
63+
t.Skip("Not supported on Windows")
64+
}
65+
66+
entrypointPath := filepath.ToSlash(filepath.Join(t.TempDir(), "entrypoint.sh"))
67+
68+
//nolint:gosec
69+
err := os.WriteFile(entrypointPath, []byte(`#!/bin/bash
70+
echo "Executing custom entrypoint"
71+
exec "$@"`), 0750)
4472
assert.NilError(t, err)
45-
}()
4673

47-
t.Run("Run task", func(t *testing.T) {
48-
select {
49-
case err = <-res.Wait():
74+
r := runner.New(
75+
"CIRCLECI_GOAT_ENTRYPOINT="+entrypointPath,
76+
"CIRCLECI_GOAT_SHUTDOWN_DELAY=10s",
77+
"CIRCLECI_GOAT_CONFIG="+goodConfig,
78+
"CIRCLECI_GOAT_HEALTH_CHECK_ADDR=:7624",
79+
)
80+
res, err := r.Start(orchestratorTestBinaryOverride)
81+
assert.NilError(t, err)
82+
83+
t.Run("Probe for readiness", func(t *testing.T) {
84+
assert.NilError(t, res.Ready("admin", time.Second*20))
85+
})
86+
87+
t.Run("Custom entrypoint ran", func(t *testing.T) {
88+
assert.Check(t, cmp.Contains(res.Logs(), "Executing custom entrypoint"))
89+
})
90+
91+
go func() {
92+
f, err := os.Create(readinessFilePath) //nolint:gosec
93+
defer func() { assert.NilError(t, f.Close()) }()
5094
assert.NilError(t, err)
51-
case <-time.After(time.Second * 40):
52-
assert.NilError(t, res.Stop())
53-
t.Fatal(t, "timeout before process stopped")
54-
}
95+
}()
96+
97+
t.Run("Run task", func(t *testing.T) {
98+
select {
99+
case err = <-res.Wait():
100+
assert.NilError(t, err)
101+
case <-time.After(time.Second * 40):
102+
assert.NilError(t, res.Stop())
103+
t.Fatal(t, "timeout before process stopped")
104+
}
105+
})
55106
})
56107

57108
// TODO: Add more test cases...

cmd/orchestrator/help_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ func TestHelp(t *testing.T) {
3434
cli: &cli.Init,
3535
wantFilename: "init.txt",
3636
},
37+
{
38+
name: "check override command help",
39+
cli: &cli.Override,
40+
wantFilename: "override.txt",
41+
},
3742
{
3843
name: "check run-task command help",
3944
cli: &cli.RunTask,

cmd/orchestrator/main.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ import (
1919
"github.com/circleci/runner-init/cmd/setup"
2020
initialize "github.com/circleci/runner-init/init"
2121
"github.com/circleci/runner-init/task"
22+
"github.com/circleci/runner-init/task/entrypoint"
2223
"github.com/circleci/runner-init/task/taskerrors"
2324
)
2425

2526
type cli struct {
2627
Version kong.VersionFlag `short:"v" help:"Print version information and quit."`
2728

28-
Init initCmd `cmd:"" name:"init" default:"withargs"`
29-
RunTask runTaskCmd `cmd:"" name:"run-task"`
29+
Init initCmd `cmd:"" name:"init" default:"withargs"`
30+
Override overrideCmd `cmd:"" name:"override"`
31+
RunTask runTaskCmd `cmd:"" name:"run-task"`
3032

3133
ShutdownDelay time.Duration `default:"0s" help:"Delay shutdown by this amount."`
3234
}
@@ -36,6 +38,12 @@ type initCmd struct {
3638
Destination string `arg:"" env:"DESTINATION" type:"path" default:"/opt/circleci/bin" help:"Path where to copy the agent binaries to."`
3739
}
3840

41+
type overrideCmd struct {
42+
Entrypoint []string `help:"Custom init process to execute as PID 1, overriding orchestrator. Must accept and execute the orchestrator command/arguments (e.g., exec \"$@\"), propagate signals, and handle standard init responsibilities like reaping zombie processes."`
43+
44+
runTaskCmd
45+
}
46+
3947
type runTaskCmd struct {
4048
TerminationGracePeriod time.Duration `default:"10s" help:"How long the agent will wait for the task to complete if interrupted."`
4149
HealthCheckAddr string `default:":7623" help:"Address for the health check API to listen on."`
@@ -87,6 +95,13 @@ func run(version, date string) (err error) {
8795
return initialize.Run(ctx, c.Source, c.Destination)
8896
})
8997

98+
case "override":
99+
ep := entrypoint.New(cli.Override.Entrypoint)
100+
sys.AddService(func(ctx context.Context) error {
101+
defer cancel()
102+
return ep.Run(ctx)
103+
})
104+
90105
case "run-task":
91106
orchestrator, err := runSetup(ctx, cli, version, sys)
92107
if err != nil {
@@ -102,9 +117,8 @@ func run(version, date string) (err error) {
102117
return sys.Run(ctx, cli.ShutdownDelay)
103118
}
104119

105-
func runSetup(ctx context.Context, cli cli, version string, sys *system.System) (*task.Orchestrator, error) {
120+
func runSetup(ctx context.Context, cli cli, version string, sys *system.System) (Runner, error) {
106121
c := cli.RunTask
107-
108122
// Strip the orchestrator configuration from the environment
109123
_ = os.Unsetenv("CIRCLECI_GOAT_CONFIG")
110124

@@ -130,3 +144,7 @@ func runSetup(ctx context.Context, cli cli, version string, sys *system.System)
130144

131145
return o, nil
132146
}
147+
148+
type Runner interface {
149+
Run(ctx context.Context) error
150+
}

cmd/orchestrator/testdata/help.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Flags:
1010
Commands:
1111
init [<source> [<destination>]] [flags]
1212

13+
override [flags]
14+
1315
run-task [flags]
1416

1517
Run "test-app <command> --help" for more information on a command.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Usage: test-app [flags]
2+
3+
Flags:
4+
-h, --help Show context-sensitive help.
5+
--entrypoint=ENTRYPOINT,...
6+
Custom init process to execute as PID 1, overriding
7+
orchestrator. Must accept and execute the orchestrator
8+
command/arguments (e.g., exec "$@"), propagate signals,
9+
and handle standard init responsibilities like reaping zombie
10+
processes ($CIRCLECI_GOAT_ENTRYPOINT).
11+
--termination-grace-period=10s
12+
How long the agent will wait for the task to complete if
13+
interrupted ($CIRCLECI_GOAT_TERMINATION_GRACE_PERIOD).
14+
--health-check-addr=":7623"
15+
Address for the health check API to listen on
16+
($CIRCLECI_GOAT_HEALTH_CHECK_ADDR).

task/cmd/cmd.go

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"io"
77
"os"
88
"os/exec"
9-
"strings"
109
"sync/atomic"
1110
"syscall"
1211
)
@@ -106,17 +105,7 @@ func newCmd(ctx context.Context, argv []string, user string, stderrSaver *prefix
106105
//#nosec:G204 // this is intentionally setting up a command
107106
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
108107

109-
for _, env := range os.Environ() {
110-
if strings.HasPrefix(env, "CIRCLECI_GOAT") {
111-
// Prevent internal configuration from being injected in the command environment
112-
continue
113-
}
114-
cmd.Env = append(cmd.Env, env)
115-
}
116-
if env != nil {
117-
cmd.Env = append(cmd.Env, env...)
118-
}
119-
108+
cmd.Env = Environ(env...)
120109
cmd.Stdout = os.Stdout
121110
cmd.Stderr = io.MultiWriter(os.Stderr, stderrSaver)
122111

task/cmd/environ.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
"strings"
6+
)
7+
8+
func Environ(extraEnv ...string) (environ []string) {
9+
for _, env := range os.Environ() {
10+
if strings.HasPrefix(env, "CIRCLECI_GOAT") {
11+
// Prevent internal configuration from being unintentionally injected in the command environment
12+
continue
13+
}
14+
environ = append(environ, env)
15+
}
16+
if extraEnv != nil {
17+
environ = append(environ, extraEnv...)
18+
}
19+
20+
return environ
21+
}

task/entrypoint/entrypoint.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package entrypoint
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"syscall"
8+
9+
"github.com/circleci/ex/o11y"
10+
)
11+
12+
type Entrypoint struct {
13+
args []string
14+
}
15+
16+
func New(args []string) Entrypoint {
17+
return Entrypoint{args}
18+
}
19+
20+
func (e Entrypoint) Run(ctx context.Context) (err error) {
21+
_, span := o11y.StartSpan(ctx, "override-entrypoint")
22+
defer o11y.End(span, &err)
23+
24+
args := append([]string{e.args[0]}, os.Args[0], "run-task")
25+
if len(e.args) > 1 {
26+
args = append(args, e.args[1:]...)
27+
}
28+
29+
//#nosec:G204 // this is intentionally setting up a command
30+
if err := syscall.Exec(e.args[0], args, os.Environ()); err != nil {
31+
return fmt.Errorf("error executing entrypoint override: %w", err)
32+
}
33+
34+
return nil
35+
}

0 commit comments

Comments
 (0)