mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-15 12:23:42 +00:00
Compare commits
4
Commits
v5.2.4
...
d8d4a4657d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8d4a4657d | ||
|
|
5fd2f82735 | ||
|
|
c6567296a0 | ||
|
|
e3607b576e |
No files matched your search
@@ -40,7 +40,9 @@ const (
|
||||
jobLabel = "com.gitea.runner.job."
|
||||
maxCreateBody = 8 << 20
|
||||
|
||||
dockerProxyProbeTimeout = 5 * time.Second
|
||||
dockerProxyProbeTimeout = 5 * time.Second
|
||||
dockerProxyProbeWorkerTimeout = time.Minute
|
||||
dockerProxyProbeTarget = "/gitea-runner-probe"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -48,7 +50,7 @@ var (
|
||||
rawStreamPath = regexp.MustCompile(`^(/v[0-9.]+)?/(containers/[^/]+/attach|exec/[^/]+/start)$`)
|
||||
)
|
||||
|
||||
var dockerProxyDirectories, dockerProxyProbes sync.Map
|
||||
var dockerProxyProbes sync.Map
|
||||
|
||||
type dockerProxyDirectory struct {
|
||||
dir, daemonDir string
|
||||
@@ -68,7 +70,7 @@ func NewDockerProxy(ctx context.Context, job string) *DockerProxy {
|
||||
}
|
||||
probeCtx, cancel := context.WithTimeout(ctx, dockerProxyProbeTimeout)
|
||||
defer cancel()
|
||||
cli, err := GetDockerClient(probeCtx)
|
||||
cli, err := client.New(client.FromEnv) // negotiates on first request, so a cached probe costs no round trip
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -83,65 +85,69 @@ func NewDockerProxy(ctx context.Context, job string) *DockerProxy {
|
||||
}
|
||||
workdir, _ := os.Getwd()
|
||||
key := [3]string{cli.DaemonHost(), workdir, os.TempDir()}
|
||||
cached, _ := dockerProxyDirectories.Load(key)
|
||||
directory, seen := cached.(dockerProxyDirectory)
|
||||
seen = seen && os.SameFile(directory.socket, socket) // daemon replacement invalidates cached paths
|
||||
if !seen {
|
||||
directory, seen, err = probeDockerProxyDirectory(probeCtx, cli, key, socket)
|
||||
}
|
||||
probe, err := probeDockerProxyDirectory(probeCtx, cli, key, socket)
|
||||
if err != nil {
|
||||
common.Logger(ctx).Infof("docker proxy probe failed, jobs get the daemon socket directly: %v", err)
|
||||
return nil
|
||||
}
|
||||
if !seen {
|
||||
if !probe.seen {
|
||||
common.Logger(ctx).Infof("the docker daemon cannot reach the runner's temporary or working directory, jobs get the daemon socket directly")
|
||||
return nil
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
proxy, err := StartDockerProxy(daemonSocket, directory.dir, job)
|
||||
proxy, err := StartDockerProxy(daemonSocket, probe.directory.dir, job)
|
||||
if err != nil {
|
||||
dockerProxyDirectories.Delete(key)
|
||||
dockerProxyProbes.CompareAndDelete(key, probe)
|
||||
common.Logger(ctx).Warnf("docker proxy not started, the job gets the daemon socket directly: %v", err)
|
||||
return nil
|
||||
}
|
||||
dockerProxyDirectories.Store(key, directory)
|
||||
proxy.Socket = directory.daemonDir + strings.TrimPrefix(proxy.Socket, directory.dir)
|
||||
proxy.Socket = probe.directory.daemonDir + strings.TrimPrefix(proxy.Socket, probe.directory.dir)
|
||||
return proxy
|
||||
}
|
||||
|
||||
func probeDockerProxyDirectory(ctx context.Context, cli client.APIClient, key [3]string, socket os.FileInfo) (dockerProxyDirectory, bool, error) {
|
||||
func probeDockerProxyDirectory(ctx context.Context, cli client.APIClient, key [3]string, socket os.FileInfo) (*dockerProxyProbe, error) {
|
||||
for {
|
||||
probe := &dockerProxyProbe{directory: dockerProxyDirectory{socket: socket}, done: make(chan struct{})}
|
||||
actual, loaded := dockerProxyProbes.LoadOrStore(key, probe)
|
||||
if loaded {
|
||||
probe, _ = actual.(*dockerProxyProbe)
|
||||
if !os.SameFile(probe.directory.socket, socket) {
|
||||
if !os.SameFile(probe.directory.socket, socket) { // daemon replacement invalidates cached paths
|
||||
dockerProxyProbes.CompareAndDelete(key, probe)
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return dockerProxyDirectory{}, false, ctx.Err()
|
||||
case <-probe.done:
|
||||
}
|
||||
if ctx.Err() == nil && (errors.Is(probe.err, context.Canceled) || errors.Is(probe.err, context.DeadlineExceeded)) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
probe.directory.dir, probe.directory.daemonDir = runnerContainerWorkdir(ctx, cli)
|
||||
probe.seen = probe.directory.daemonDir != ""
|
||||
if !probe.seen {
|
||||
if probe.directory.dir, probe.err = filepath.Abs(key[2]); probe.err == nil {
|
||||
probe.directory.daemonDir = probe.directory.dir
|
||||
probe.seen, probe.err = daemonSeesDir(ctx, cli, probe.directory.dir, probe.directory.daemonDir)
|
||||
go func() { // outlives callers' deadlines so a slow daemon still gets its result cached
|
||||
defer cli.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dockerProxyProbeWorkerTimeout)
|
||||
defer cancel()
|
||||
probe.directory.dir, probe.directory.daemonDir = runnerContainerWorkdir(ctx, cli)
|
||||
probe.seen = probe.directory.daemonDir != ""
|
||||
if !probe.seen {
|
||||
if probe.directory.dir, probe.err = filepath.Abs(key[2]); probe.err == nil {
|
||||
probe.directory.daemonDir = probe.directory.dir
|
||||
probe.seen, probe.err = daemonSeesDir(ctx, cli, probe.directory.dir, probe.directory.daemonDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
dockerProxyProbes.CompareAndDelete(key, probe)
|
||||
close(probe.done)
|
||||
if probe.seen {
|
||||
dockerProxyProbes.LoadOrStore(key, probe)
|
||||
} else {
|
||||
dockerProxyProbes.CompareAndDelete(key, probe)
|
||||
}
|
||||
close(probe.done)
|
||||
removeStaleProbeContainers(cli)
|
||||
}()
|
||||
}
|
||||
return probe.directory, probe.seen, probe.err
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &dockerProxyProbe{}, ctx.Err()
|
||||
case <-probe.done:
|
||||
}
|
||||
if ctx.Err() == nil && errors.Is(probe.err, context.DeadlineExceeded) {
|
||||
continue
|
||||
}
|
||||
return probe, probe.err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +206,7 @@ func daemonSeesDir(ctx context.Context, cli client.APIClient, dir, daemonDir str
|
||||
created, err = cli.ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
Config: &container.Config{Image: image.ID, Cmd: []string{"true"}},
|
||||
HostConfig: &container.HostConfig{Mounts: []mount.Mount{
|
||||
{Type: mount.TypeBind, Source: daemonDir, Target: "/gitea-runner-probe", ReadOnly: true}, // not the marker itself, podman creates a missing bind source where docker rejects it
|
||||
{Type: mount.TypeBind, Source: daemonDir, Target: dockerProxyProbeTarget, ReadOnly: true}, // not the marker itself, podman creates a missing bind source where docker rejects it
|
||||
}},
|
||||
})
|
||||
if !cerrdefs.IsNotFound(err) {
|
||||
@@ -213,7 +219,7 @@ func daemonSeesDir(ctx context.Context, cli client.APIClient, dir, daemonDir str
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
seen, err := containerSeesMarker(ctx, cli, dir, created.ID, "/gitea-runner-probe")
|
||||
seen, err := containerSeesMarker(ctx, cli, dir, created.ID, dockerProxyProbeTarget)
|
||||
cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), dockerProxyProbeTimeout)
|
||||
defer cancel()
|
||||
if _, removeErr := cli.ContainerRemove(cleanupCtx, created.ID, client.ContainerRemoveOptions{Force: true, RemoveVolumes: true}); removeErr != nil {
|
||||
@@ -222,6 +228,36 @@ func daemonSeesDir(ctx context.Context, cli client.APIClient, dir, daemonDir str
|
||||
return seen, err
|
||||
}
|
||||
|
||||
// removeStaleProbeContainers removes probe containers left by abandoned creates once any probe in flight when listed has ended.
|
||||
func removeStaleProbeContainers(cli client.APIClient) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dockerProxyProbeTimeout)
|
||||
defer cancel()
|
||||
listed, err := cli.ContainerList(ctx, client.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: make(client.Filters).Add("status", string(container.StateCreated)).Add("volume", dockerProxyProbeTarget),
|
||||
})
|
||||
if err != nil {
|
||||
common.Logger(ctx).Warnf("listing stale docker proxy probe containers failed: %v", err)
|
||||
return
|
||||
}
|
||||
listed.Items = slices.DeleteFunc(listed.Items, func(stale container.Summary) bool {
|
||||
return stale.State != container.StateCreated || !slices.ContainsFunc(stale.Mounts, func(mountPoint container.MountPoint) bool {
|
||||
return mountPoint.Type == mount.TypeBind && !mountPoint.RW && mountPoint.Destination == dockerProxyProbeTarget
|
||||
})
|
||||
})
|
||||
if len(listed.Items) == 0 {
|
||||
return
|
||||
}
|
||||
time.Sleep(dockerProxyProbeWorkerTimeout + dockerProxyProbeTimeout)
|
||||
for _, stale := range listed.Items {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dockerProxyProbeTimeout)
|
||||
if err := (&containerReference{cli: cli, id: stale.ID}).remove()(ctx); err != nil {
|
||||
common.Logger(ctx).Warnf("removing stale docker proxy probe container failed: %v", err)
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// StartDockerProxy serves a job's docker socket in dir, labelling what the job creates through it.
|
||||
func StartDockerProxy(daemonSocket, dir, job string) (*DockerProxy, error) {
|
||||
info, err := os.Stat(daemonSocket)
|
||||
|
||||
@@ -94,6 +94,8 @@ func TestDockerProxy(t *testing.T) {
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`[{"Id":"probe-image"}]`))
|
||||
case strings.HasSuffix(r.URL.Path, "/containers/json"):
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
case strings.HasSuffix(r.URL.Path, "/json"):
|
||||
http.NotFound(w, r)
|
||||
case strings.HasSuffix(r.URL.Path, "/archive"):
|
||||
@@ -235,7 +237,8 @@ func TestDockerProxy(t *testing.T) {
|
||||
t.Skip("automatic proxy discovery requires Linux")
|
||||
}
|
||||
t.Setenv("DOCKER_HOST", "unix://"+daemonSocket)
|
||||
t.Setenv("TMPDIR", shortTempDir(t))
|
||||
tempDir := shortTempDir(t)
|
||||
t.Setenv("TMPDIR", tempDir)
|
||||
require.Nil(t, NewDockerProxy(t.Context(), "failed"))
|
||||
for _, job := range []string{"first", "second"} {
|
||||
proxy := NewDockerProxy(t.Context(), job)
|
||||
@@ -243,12 +246,10 @@ func TestDockerProxy(t *testing.T) {
|
||||
require.NoError(t, proxy.Close(context.Background()))
|
||||
}
|
||||
assert.EqualValues(t, 2, probes.Load())
|
||||
dockerProxyDirectories.Range(func(key, value any) bool {
|
||||
directory, _ := value.(dockerProxyDirectory)
|
||||
directory.socket, _ = os.Stat(t.TempDir())
|
||||
dockerProxyDirectories.Store(key, directory)
|
||||
return true
|
||||
})
|
||||
workdir, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
cached, _ := dockerProxyProbes.Load([3]string{"unix://" + daemonSocket, workdir, tempDir})
|
||||
cached.(*dockerProxyProbe).directory.socket, _ = os.Stat(t.TempDir())
|
||||
proxy := NewDockerProxy(t.Context(), "replaced daemon")
|
||||
require.NotNil(t, proxy)
|
||||
require.NoError(t, proxy.Close(context.Background()))
|
||||
@@ -476,10 +477,23 @@ func BenchmarkDockerProxy(b *testing.B) {
|
||||
|
||||
type probeClient struct {
|
||||
mobyclient.APIClient
|
||||
create func(mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error)
|
||||
remove func(context.Context, string, mobyclient.ContainerRemoveOptions) (mobyclient.ContainerRemoveResult, error)
|
||||
stat func(string) error
|
||||
mounts []container.MountPoint
|
||||
create func(context.Context, mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error)
|
||||
remove func(context.Context, string, mobyclient.ContainerRemoveOptions) (mobyclient.ContainerRemoveResult, error)
|
||||
stat func(string) error
|
||||
mounts []container.MountPoint
|
||||
containers []container.Summary
|
||||
}
|
||||
|
||||
func (c *probeClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *probeClient) ContainerList(context.Context, mobyclient.ContainerListOptions) (mobyclient.ContainerListResult, error) {
|
||||
return mobyclient.ContainerListResult{Items: c.containers}, nil
|
||||
}
|
||||
|
||||
func (c *probeClient) ContainerKill(context.Context, string, mobyclient.ContainerKillOptions) (mobyclient.ContainerKillResult, error) {
|
||||
return mobyclient.ContainerKillResult{}, cerrdefs.ErrConflict
|
||||
}
|
||||
|
||||
func (c *probeClient) ContainerInspect(context.Context, string, mobyclient.ContainerInspectOptions) (mobyclient.ContainerInspectResult, error) {
|
||||
@@ -490,8 +504,8 @@ func (c *probeClient) ImageList(context.Context, mobyclient.ImageListOptions) (m
|
||||
return mobyclient.ImageListResult{Items: []image.Summary{{ID: "removed-image"}, {ID: "probe-image"}}}, nil
|
||||
}
|
||||
|
||||
func (c *probeClient) ContainerCreate(_ context.Context, opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
return c.create(opts)
|
||||
func (c *probeClient) ContainerCreate(ctx context.Context, opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
return c.create(ctx, opts)
|
||||
}
|
||||
|
||||
func (c *probeClient) ContainerRemove(ctx context.Context, id string, opts mobyclient.ContainerRemoveOptions) (mobyclient.ContainerRemoveResult, error) {
|
||||
@@ -518,7 +532,7 @@ func TestDockerProxyDirectoryProbes(t *testing.T) {
|
||||
var calls, removals atomic.Int32
|
||||
visible := false
|
||||
cli := &probeClient{
|
||||
create: func(_opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
create: func(_ctx context.Context, _opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
calls.Add(1)
|
||||
<-release
|
||||
return mobyclient.ContainerCreateResult{ID: "probe"}, createErr
|
||||
@@ -538,23 +552,18 @@ func TestDockerProxyDirectoryProbes(t *testing.T) {
|
||||
results := make(chan result, 8)
|
||||
for range cap(results) {
|
||||
go func() {
|
||||
_, seen, err := probeDockerProxyDirectory(t.Context(), cli, key, socket)
|
||||
results <- result{seen, err}
|
||||
probe, err := probeDockerProxyDirectory(t.Context(), cli, key, socket)
|
||||
results <- result{probe.seen, err}
|
||||
}()
|
||||
}
|
||||
synctest.Wait()
|
||||
assert.EqualValues(t, 1, calls.Load())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
_, seen, err := probeDockerProxyDirectory(ctx, cli, key, socket)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
assert.False(t, seen)
|
||||
replacement, err := os.Stat(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
replaced := make(chan result, 1)
|
||||
go func() {
|
||||
_, seen, err := probeDockerProxyDirectory(t.Context(), cli, key, replacement)
|
||||
replaced <- result{seen, err}
|
||||
probe, err := probeDockerProxyDirectory(t.Context(), cli, key, replacement)
|
||||
replaced <- result{probe.seen, err}
|
||||
}()
|
||||
synctest.Wait()
|
||||
assert.EqualValues(t, 2, calls.Load())
|
||||
@@ -574,11 +583,11 @@ func TestDockerProxyDirectoryProbes(t *testing.T) {
|
||||
assert.Zero(t, removals.Load())
|
||||
}
|
||||
createErr, visible = nil, true
|
||||
directory, seen, err := probeDockerProxyDirectory(t.Context(), cli, key, socket)
|
||||
probe, err := probeDockerProxyDirectory(t.Context(), cli, key, socket)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, seen)
|
||||
assert.Equal(t, dir, directory.dir)
|
||||
assert.Equal(t, dir, directory.daemonDir)
|
||||
assert.True(t, probe.seen)
|
||||
assert.Equal(t, dir, probe.directory.dir)
|
||||
assert.Equal(t, dir, probe.directory.daemonDir)
|
||||
assert.EqualValues(t, 3, calls.Load())
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
@@ -586,7 +595,7 @@ func TestDockerProxyDirectoryProbes(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
t.Run("cancelled probe does not cancel another caller", func(t *testing.T) {
|
||||
t.Run("cancelled caller leaves the probe to finish for the others", func(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
socket, err := os.Stat(t.TempDir())
|
||||
@@ -595,49 +604,105 @@ func TestDockerProxyDirectoryProbes(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
release := make(chan struct{})
|
||||
calls := 0
|
||||
var calls, removals atomic.Int32
|
||||
cli := &probeClient{
|
||||
create: func(_opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
<-release
|
||||
create: func(ctx context.Context, _opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
calls.Add(1)
|
||||
<-release
|
||||
return mobyclient.ContainerCreateResult{ID: "probe"}, ctx.Err()
|
||||
},
|
||||
stat: func(_path string) error { return nil },
|
||||
remove: func(_ctx context.Context, _id string, _opts mobyclient.ContainerRemoveOptions) (mobyclient.ContainerRemoveResult, error) {
|
||||
removals.Add(1)
|
||||
return mobyclient.ContainerRemoveResult{}, nil
|
||||
},
|
||||
}
|
||||
leader, follower, late := make(chan result, 1), make(chan result, 1), make(chan result, 1)
|
||||
go func() {
|
||||
probe, err := probeDockerProxyDirectory(ctx, cli, key, socket)
|
||||
leader <- result{probe.seen, err}
|
||||
}()
|
||||
synctest.Wait()
|
||||
go func() {
|
||||
probe, err := probeDockerProxyDirectory(t.Context(), cli, key, socket)
|
||||
follower <- result{probe.seen, err}
|
||||
}()
|
||||
synctest.Wait()
|
||||
cancel()
|
||||
leaderResult := <-leader
|
||||
require.ErrorIs(t, leaderResult.err, context.Canceled)
|
||||
assert.False(t, leaderResult.seen)
|
||||
go func() {
|
||||
probe, err := probeDockerProxyDirectory(t.Context(), cli, key, socket)
|
||||
late <- result{probe.seen, err}
|
||||
}()
|
||||
synctest.Wait()
|
||||
close(release)
|
||||
for _, results := range []chan result{follower, late} {
|
||||
result := <-results
|
||||
require.NoError(t, result.err)
|
||||
assert.True(t, result.seen)
|
||||
}
|
||||
assert.EqualValues(t, 1, calls.Load())
|
||||
assert.EqualValues(t, 1, removals.Load())
|
||||
_, cached := dockerProxyProbes.Load(key)
|
||||
assert.True(t, cached)
|
||||
})
|
||||
})
|
||||
t.Run("caller joining a probe that ran out of time retries", func(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
socket, err := os.Stat(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
var calls atomic.Int32
|
||||
cli := &probeClient{
|
||||
create: func(ctx context.Context, _opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
if calls.Add(1) == 1 {
|
||||
<-ctx.Done()
|
||||
return mobyclient.ContainerCreateResult{}, ctx.Err()
|
||||
} else {
|
||||
return mobyclient.ContainerCreateResult{ID: "probe"}, nil
|
||||
}
|
||||
return mobyclient.ContainerCreateResult{ID: "probe"}, nil
|
||||
},
|
||||
stat: func(_path string) error { return nil },
|
||||
remove: func(_ctx context.Context, _id string, _opts mobyclient.ContainerRemoveOptions) (mobyclient.ContainerRemoveResult, error) {
|
||||
return mobyclient.ContainerRemoveResult{}, nil
|
||||
},
|
||||
}
|
||||
leader, follower := make(chan result, 1), make(chan result, 1)
|
||||
go func() {
|
||||
_, seen, err := probeDockerProxyDirectory(ctx, cli, key, socket)
|
||||
leader <- result{seen, err}
|
||||
}()
|
||||
synctest.Wait()
|
||||
go func() {
|
||||
_, seen, err := probeDockerProxyDirectory(t.Context(), cli, key, socket)
|
||||
follower <- result{seen, err}
|
||||
}()
|
||||
synctest.Wait()
|
||||
assert.Equal(t, 1, calls)
|
||||
cancel()
|
||||
close(release)
|
||||
leaderResult, followerResult := <-leader, <-follower
|
||||
require.ErrorIs(t, leaderResult.err, context.Canceled)
|
||||
assert.False(t, leaderResult.seen)
|
||||
require.NoError(t, followerResult.err)
|
||||
assert.True(t, followerResult.seen)
|
||||
assert.Equal(t, 2, calls)
|
||||
probe, err := probeDockerProxyDirectory(t.Context(), cli, [3]string{"daemon", dir, dir}, socket)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, probe.seen)
|
||||
assert.EqualValues(t, 2, calls.Load())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveStaleProbeContainers(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
probeMount := container.MountPoint{Type: mount.TypeBind, Destination: "/gitea-runner-probe"}
|
||||
writableMount := container.MountPoint{Type: mount.TypeBind, Destination: "/gitea-runner-probe", RW: true}
|
||||
var removed []string
|
||||
cli := &probeClient{
|
||||
containers: []container.Summary{
|
||||
{ID: "abandoned", State: container.StateCreated, Mounts: []container.MountPoint{probeMount}},
|
||||
{ID: "foreign", State: container.StateCreated, Mounts: []container.MountPoint{writableMount}},
|
||||
{ID: "started", State: container.StateExited, Mounts: []container.MountPoint{probeMount}},
|
||||
},
|
||||
remove: func(_ctx context.Context, id string, _opts mobyclient.ContainerRemoveOptions) (mobyclient.ContainerRemoveResult, error) {
|
||||
removed = append(removed, id)
|
||||
return mobyclient.ContainerRemoveResult{}, nil
|
||||
},
|
||||
}
|
||||
start := time.Now()
|
||||
removeStaleProbeContainers(cli)
|
||||
assert.Equal(t, []string{"abandoned"}, removed)
|
||||
assert.GreaterOrEqual(t, time.Since(start), dockerProxyProbeWorkerTimeout+dockerProxyProbeTimeout)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaemonSeesDir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
markers := make(map[string]bool)
|
||||
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
daemonDir string
|
||||
@@ -656,7 +721,7 @@ func TestDaemonSeesDir(t *testing.T) {
|
||||
removed := false
|
||||
var attemptedImages []string
|
||||
cli := &probeClient{
|
||||
create: func(opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
create: func(_ctx context.Context, opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
assert.Equal(t, []mount.Mount{{Type: mount.TypeBind, Source: testCase.daemonDir, Target: "/gitea-runner-probe", ReadOnly: true}}, opts.HostConfig.Mounts)
|
||||
attemptedImages = append(attemptedImages, opts.Config.Image)
|
||||
if opts.Config.Image == "removed-image" {
|
||||
@@ -692,6 +757,7 @@ func TestDaemonSeesDir(t *testing.T) {
|
||||
assert.Equal(t, []string{"removed-image", "probe-image"}, attemptedImages)
|
||||
assert.Equal(t, testCase.seen, seen)
|
||||
assert.Equal(t, testCase.removed, removed)
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, entries)
|
||||
|
||||
@@ -980,7 +980,7 @@ func TestDockerCreateKeepsInputEntrypoint(t *testing.T) {
|
||||
RunnerOptions: "--entrypoint=runner-entrypoint",
|
||||
WorkflowOptions: "--entrypoint=" + optionEntrypoint + " --init",
|
||||
},
|
||||
cli: &probeClient{create: func(opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
cli: &probeClient{create: func(_ctx context.Context, opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
if len(entrypoint) > 0 {
|
||||
assert.Equal(t, entrypoint, opts.Config.Entrypoint)
|
||||
} else {
|
||||
|
||||
@@ -180,9 +180,9 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
|
||||
|
||||
action := step.getActionModel()
|
||||
|
||||
rc.withGithubEnv(ctx, step.getGithubContext(ctx), *step.getEnv())
|
||||
rc.withGithubEnv(ctx, getStepGithubContext(ctx, step), *step.getEnv())
|
||||
populateEnvsFromSavedState(step.getEnv(), step, rc)
|
||||
if err := populateEnvsFromInput(ctx, step.getEnv(), action, rc); err != nil {
|
||||
if err := populateEnvsFromInput(ctx, step, action); err != nil {
|
||||
return err
|
||||
}
|
||||
warnUnexpectedInputs(ctx, action, stepModel.With)
|
||||
@@ -283,7 +283,7 @@ func (rc *RunContext) dockerActionImage(ctx context.Context, step actionStep, ac
|
||||
}
|
||||
image, ok := job.dockerActionImages[step.getStepModel()]
|
||||
if !ok {
|
||||
image = dockerActionImageTag(step.getGithubContext(ctx).Repository, actionName, localAction) + "-" + strings.ToLower(rand.Text())
|
||||
image = dockerActionImageTag(getStepGithubContext(ctx, step).Repository, actionName, localAction) + "-" + strings.ToLower(rand.Text())
|
||||
if !common.Dryrun(ctx) { // a dry run builds nothing to remove
|
||||
job.dockerActionImages[step.getStepModel()] = image
|
||||
}
|
||||
@@ -437,7 +437,7 @@ func evalDockerEnv(ctx context.Context, step step, action *model.Action) (*expre
|
||||
for key := range step.getStepModel().With {
|
||||
providedInputs[strings.ToLower(key)] = true
|
||||
}
|
||||
eval := rc.NewExpressionEvaluator(ctx)
|
||||
eval := rc.NewStepExpressionEvaluator(ctx, step)
|
||||
for k, input := range action.Inputs {
|
||||
if providedInputs[strings.ToLower(k)] {
|
||||
continue
|
||||
@@ -509,8 +509,9 @@ func inputEnvKey(inputID string) string {
|
||||
return "INPUT_" + inputEnvPattern.ReplaceAllString(strings.ToUpper(inputID), "_")
|
||||
}
|
||||
|
||||
func populateEnvsFromInput(ctx context.Context, env *map[string]string, action *model.Action, rc *RunContext) error {
|
||||
eval := rc.NewExpressionEvaluator(ctx)
|
||||
func populateEnvsFromInput(ctx context.Context, step step, action *model.Action) error {
|
||||
env := step.getEnv()
|
||||
eval := step.getRunContext().NewStepExpressionEvaluator(ctx, step)
|
||||
for inputID, input := range action.Inputs {
|
||||
envKey := inputEnvKey(inputID)
|
||||
if _, ok := (*env)[envKey]; !ok {
|
||||
@@ -652,7 +653,7 @@ func runPreStep(step actionStep) common.Executor {
|
||||
x := action.Runs.Using
|
||||
if !x.IsComposite() {
|
||||
// defaults in pre steps were missing, however provided inputs are available
|
||||
if err := populateEnvsFromInput(ctx, step.getEnv(), action, rc); err != nil {
|
||||
if err := populateEnvsFromInput(ctx, step, action); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,9 +57,6 @@ func evaluateCompositeInputAndEnv(ctx context.Context, parent *RunContext, step
|
||||
env[envKey] = value
|
||||
}
|
||||
}
|
||||
gh := step.getGithubContext(ctx)
|
||||
env["GITHUB_ACTION_REPOSITORY"] = gh.ActionRepository
|
||||
env["GITHUB_ACTION_REF"] = gh.ActionRef
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,31 @@ import (
|
||||
)
|
||||
|
||||
func TestCompositeActionParity(t *testing.T) {
|
||||
t.Run("input defaults use the step identity", func(t *testing.T) {
|
||||
for _, uses := range []string{"./action", "own/action@v2"} {
|
||||
t.Run(uses, func(t *testing.T) {
|
||||
rc := &RunContext{
|
||||
Config: &Config{PresetGitHubContext: &model.GithubContext{}},
|
||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
||||
Env: map[string]string{"GITHUB_ACTION_REPOSITORY": "other/composite", "GITHUB_ACTION_REF": "v1"},
|
||||
}
|
||||
var currentStep step = &stepActionLocal{RunContext: rc, Step: &model.Step{Uses: uses}, env: map[string]string{}}
|
||||
want := "|"
|
||||
if uses == "own/action@v2" {
|
||||
currentStep = &stepActionRemote{RunContext: rc, Step: &model.Step{Uses: uses}, env: map[string]string{}, remoteAction: mustNewRemoteAction(t, uses)}
|
||||
want = "own/action|v2"
|
||||
}
|
||||
action := &model.Action{Inputs: map[string]model.Input{"identity": {Default: "${{ github.action_repository }}|${{ github.action_ref }}"}}}
|
||||
require.NoError(t, setupEnv(t.Context(), currentStep))
|
||||
require.NoError(t, populateEnvsFromInput(t.Context(), currentStep, action))
|
||||
assert.Equal(t, want, (*currentStep.getEnv())["INPUT_IDENTITY"])
|
||||
_, err := evalDockerEnv(t.Context(), currentStep, action)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, (*currentStep.getEnv())["identity"])
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("inherits contexts without leaking inputs", func(t *testing.T) {
|
||||
hook := logtest.NewWithLevel(logging.LevelWarn)
|
||||
ctx := common.WithLogger(t.Context(), logging.New(hook))
|
||||
@@ -54,7 +79,7 @@ func TestCompositeActionParity(t *testing.T) {
|
||||
assert.NotContains(t, composite.Env, "INPUT_EXTRA")
|
||||
|
||||
nestedEnv := composite.GetEnv()
|
||||
require.NoError(t, populateEnvsFromInput(ctx, &nestedEnv, &model.Action{Inputs: map[string]model.Input{"shared": {Default: "inner-default"}}}, composite))
|
||||
require.NoError(t, populateEnvsFromInput(ctx, &stepActionLocal{RunContext: composite, env: nestedEnv}, &model.Action{Inputs: map[string]model.Input{"shared": {Default: "inner-default"}}}))
|
||||
assert.Equal(t, "inner-default", nestedEnv["INPUT_SHARED"])
|
||||
})
|
||||
|
||||
|
||||
@@ -90,18 +90,18 @@ var hashfiles string
|
||||
|
||||
// NewStepExpressionEvaluator creates a new evaluator with the `inputs` of the enclosing workflow or composite action
|
||||
func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step) *ExpressionEvaluator {
|
||||
return rc.newStepExpressionEvaluator(ctx, step, rc.actionInputs, rc.getJobContext())
|
||||
return rc.newStepExpressionEvaluator(ctx, step, rc.actionInputs, rc.getJobContext(), getStepGithubContext(ctx, step))
|
||||
}
|
||||
|
||||
// NewActionInputsExpressionEvaluator creates a new evaluator with the step's own with: values as `inputs`
|
||||
func (rc *RunContext) NewActionInputsExpressionEvaluator(ctx context.Context, step step) *ExpressionEvaluator {
|
||||
return rc.newStepExpressionEvaluator(ctx, step, inputsFromEnv(*step.getEnv()), rc.getJobContext())
|
||||
return rc.newStepExpressionEvaluator(ctx, step, inputsFromEnv(*step.getEnv()), rc.getJobContext(), getStepGithubContext(ctx, step))
|
||||
}
|
||||
|
||||
func (rc *RunContext) newStepExpressionEvaluator(ctx context.Context, step step, stepInputs map[string]any, jobContext *model.JobContext) *ExpressionEvaluator {
|
||||
func (rc *RunContext) newStepExpressionEvaluator(ctx context.Context, step step, stepInputs map[string]any, jobContext *model.JobContext, github *model.GithubContext) *ExpressionEvaluator {
|
||||
// todo: cleanup EvaluationEnvironment creation
|
||||
ee := &exprparser.EvaluationEnvironment{
|
||||
Github: step.getGithubContext(ctx),
|
||||
Github: github,
|
||||
Env: *step.getEnv(),
|
||||
Job: jobContext,
|
||||
Steps: rc.StepResults,
|
||||
@@ -112,7 +112,7 @@ func (rc *RunContext) newStepExpressionEvaluator(ctx context.Context, step step,
|
||||
Needs: exprparser.NeedsContext(rc.Run),
|
||||
// todo: should be unavailable
|
||||
// but required to interpolate/evaluate the inputs in actions/composite
|
||||
Inputs: getEvaluatorInputs(rc, stepInputs, rc.getGithubContext(ctx)),
|
||||
Inputs: getEvaluatorInputs(rc, stepInputs, github),
|
||||
HashFiles: getHashFilesFunction(ctx, rc),
|
||||
}
|
||||
ee.Runner = rc.getRunnerContext(ctx)
|
||||
|
||||
@@ -111,7 +111,7 @@ func printPrepareActions(rc *RunContext, preparers []actionPreparer) common.Exec
|
||||
if !ok || strings.Contains(action.Step.Uses, "${{") || action.action != nil {
|
||||
continue
|
||||
}
|
||||
github := action.getGithubContext(common.WithLogger(ctx, logging.New(logging.DiscardHandler)))
|
||||
github := action.RunContext.getGithubContext(common.WithLogger(ctx, logging.New(logging.DiscardHandler)))
|
||||
remote, err := newRemoteAction(action.Step.Uses, github, action.RunContext.Config.GitHubInstance)
|
||||
if err != nil || action.skipsLocalCheckout(remote, github) {
|
||||
continue
|
||||
|
||||
+17
-3
@@ -27,12 +27,24 @@ type step interface {
|
||||
post() common.Executor
|
||||
|
||||
getRunContext() *RunContext
|
||||
getGithubContext(ctx context.Context) *model.GithubContext
|
||||
getStepModel() *model.Step
|
||||
getEnv() *map[string]string
|
||||
getIfExpression(context context.Context, stage stepStage) string
|
||||
}
|
||||
|
||||
func stepActionIdentity(step step) (repository, ref string) {
|
||||
if action, ok := step.(interface{ actionIdentity() (string, string) }); ok {
|
||||
return action.actionIdentity()
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func getStepGithubContext(ctx context.Context, step step) *model.GithubContext {
|
||||
github := step.getRunContext().getGithubContext(ctx)
|
||||
github.ActionRepository, github.ActionRef = stepActionIdentity(step)
|
||||
return github
|
||||
}
|
||||
|
||||
type stepStage int
|
||||
|
||||
const (
|
||||
@@ -92,6 +104,8 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
runStep, err = isStepEnabled(ctx, ifExpression, step, stage)
|
||||
}
|
||||
if err == nil && runStep {
|
||||
env := *step.getEnv()
|
||||
env["GITHUB_ACTION_REPOSITORY"], env["GITHUB_ACTION_REF"] = stepActionIdentity(step)
|
||||
err = setupInputs(ctx, step)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -253,7 +267,7 @@ func setupEnv(ctx context.Context, step step) error {
|
||||
env := step.getEnv()
|
||||
// container env is the image's baseline, which job env and $GITHUB_ENV override
|
||||
mergeIntoMap(step, env, rc.containerSpec.Env, rc.GetEnv())
|
||||
rc.withGithubEnv(ctx, step.getGithubContext(ctx), *env)
|
||||
rc.withGithubEnv(ctx, getStepGithubContext(ctx, step), *env)
|
||||
|
||||
var err error
|
||||
exprEval := rc.NewExpressionEvaluator(ctx)
|
||||
@@ -302,7 +316,7 @@ func isStepEnabled(ctx context.Context, expr string, step step, stage stepStage)
|
||||
if rc.Parent != nil && stage == stepStageMain && jobContext.Status != "cancelled" {
|
||||
jobContext.Status = rc.ownStatus()
|
||||
}
|
||||
runStep, err := EvalBool(ctx, rc.newStepExpressionEvaluator(ctx, step, rc.actionInputs, jobContext), expr, defaultStatusCheck)
|
||||
runStep, err := EvalBool(ctx, rc.newStepExpressionEvaluator(ctx, step, rc.actionInputs, jobContext, rc.getGithubContext(ctx)), expr, defaultStatusCheck)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("if-expression %q evaluation failed: %s", expr, err)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (sab *stepActionBuiltin) main() common.Executor {
|
||||
rc.ApplyExtraPath(ctx, &sab.env)
|
||||
return sab.run(ctx, &action.Context{
|
||||
Container: rc.JobContainer,
|
||||
Github: rc.getGithubContext(ctx),
|
||||
Github: getStepGithubContext(ctx, sab),
|
||||
Inputs: sab.Step.With,
|
||||
HostWorkdir: rc.Config.Workdir,
|
||||
Workspace: rc.JobContainer.ToContainerPath(rc.Config.Workdir),
|
||||
@@ -58,10 +58,6 @@ func (sab *stepActionBuiltin) post() common.Executor { return common.NewPipeline
|
||||
|
||||
func (sab *stepActionBuiltin) getRunContext() *RunContext { return sab.RunContext }
|
||||
|
||||
func (sab *stepActionBuiltin) getGithubContext(ctx context.Context) *model.GithubContext {
|
||||
return sab.RunContext.getGithubContext(ctx)
|
||||
}
|
||||
|
||||
func (sab *stepActionBuiltin) getStepModel() *model.Step { return sab.Step }
|
||||
|
||||
func (sab *stepActionBuiltin) getEnv() *map[string]string { return &sab.env }
|
||||
|
||||
@@ -95,10 +95,6 @@ func (sal *stepActionLocal) getRunContext() *RunContext {
|
||||
return sal.RunContext
|
||||
}
|
||||
|
||||
func (sal *stepActionLocal) getGithubContext(ctx context.Context) *model.GithubContext {
|
||||
return sal.getRunContext().getGithubContext(ctx)
|
||||
}
|
||||
|
||||
func (sal *stepActionLocal) getStepModel() *model.Step {
|
||||
return sal.Step
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type stepActionMocks struct {
|
||||
@@ -53,7 +54,7 @@ func TestStepActionLocalTest(t *testing.T) {
|
||||
readAction: salm.readAction,
|
||||
runAction: salm.runAction,
|
||||
RunContext: &RunContext{
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
StepResults: map[string]*model.StepResult{"previous": {Outputs: map[string]string{"value": "previous"}}},
|
||||
ExprEval: &expressionEvaluator{},
|
||||
Config: &Config{
|
||||
Workdir: "/tmp",
|
||||
@@ -68,25 +69,42 @@ func TestStepActionLocalTest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
JobContainer: cm,
|
||||
Env: map[string]string{"GITHUB_ACTION_REPOSITORY": "other/action", "GITHUB_ACTION_REF": "v1"},
|
||||
Matrix: map[string]any{"value": "matrix"},
|
||||
actionInputs: map[string]any{"shared": "input"},
|
||||
},
|
||||
Step: &model.Step{
|
||||
ID: "1",
|
||||
Uses: "./path/to/action",
|
||||
If: yaml.Node{Value: "github.action_repository == 'other/action' && github.action_ref == 'v1' && " +
|
||||
"env.STEP_VALUE == 'step' && env.GITHUB_ACTION_REPOSITORY == 'forged/repo' && env.GITHUB_ACTION_REF == 'forged-ref' && " +
|
||||
"steps.previous.outputs.value == 'previous' && inputs.shared == 'input' && matrix.value == 'matrix'"},
|
||||
},
|
||||
}
|
||||
require.NoError(t, sal.Step.Env.Encode(map[string]string{"STEP_VALUE": "step", "GITHUB_ACTION_REPOSITORY": "forged/repo", "GITHUB_ACTION_REF": "forged-ref"}))
|
||||
require.NoError(t, sal.Step.RawWith.Encode(map[string]string{"identity": "${{ github.action_repository }}|${{ github.action_ref }}"}))
|
||||
|
||||
salm.On("readAction", sal.Step, filepath.Clean("/tmp/path/to/action"), "", mock.Anything, mock.Anything).
|
||||
Return(&model.Action{}, nil)
|
||||
|
||||
expectStepFileCommands(t, cm)
|
||||
|
||||
salm.On("runAction", sal, filepath.Clean("/tmp/path/to/action"), (*remoteAction)(nil)).Return(noopExecutor)
|
||||
salm.On("runAction", sal, filepath.Clean("/tmp/path/to/action"), (*remoteAction)(nil)).Return(func(ctx context.Context) error {
|
||||
sal.RunContext.setOutput(ctx, map[string]string{"name": "value"}, "ran")
|
||||
return nil
|
||||
})
|
||||
|
||||
err := sal.pre()(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sal.main()(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, sal.env["GITHUB_ACTION_REPOSITORY"])
|
||||
assert.Empty(t, sal.env["GITHUB_ACTION_REF"])
|
||||
assert.Equal(t, "|", sal.Step.With["identity"])
|
||||
assert.Equal(t, "ran", sal.RunContext.StepResults["1"].Outputs["value"])
|
||||
assert.Equal(t, "other/action", sal.RunContext.Env["GITHUB_ACTION_REPOSITORY"])
|
||||
assert.Equal(t, "v1", sal.RunContext.Env["GITHUB_ACTION_REF"])
|
||||
|
||||
cm.AssertExpectations(t)
|
||||
salm.AssertExpectations(t)
|
||||
|
||||
@@ -75,7 +75,7 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
||||
}
|
||||
sar.Step.Uses = uses
|
||||
|
||||
github := sar.getGithubContext(ctx) // read before remoteAction is set, so `$/` resolves against the enclosing action
|
||||
github := sar.RunContext.getGithubContext(ctx)
|
||||
if sar.remoteAction, err = newRemoteAction(sar.Step.Uses, github, sar.RunContext.Config.GitHubInstance); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -153,7 +153,7 @@ func (sar *stepActionRemote) main() common.Executor {
|
||||
return common.NewPipelineExecutor(
|
||||
sar.prepareActionExecutor(),
|
||||
runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
|
||||
github := sar.getGithubContext(ctx)
|
||||
github := getStepGithubContext(ctx, sar)
|
||||
if sar.skipsLocalCheckout(sar.remoteAction, github) {
|
||||
printRunActionHeader(ctx, sar.Step, nil, sar.env, sar.RunContext)
|
||||
if sar.RunContext.Config.BindWorkdir {
|
||||
@@ -181,17 +181,11 @@ func (sar *stepActionRemote) getRunContext() *RunContext {
|
||||
return sar.RunContext
|
||||
}
|
||||
|
||||
func (sar *stepActionRemote) getGithubContext(ctx context.Context) *model.GithubContext {
|
||||
ghc := sar.getRunContext().getGithubContext(ctx)
|
||||
|
||||
// extend github context if we already have an initialized remoteAction
|
||||
remoteAction := sar.remoteAction
|
||||
if remoteAction != nil {
|
||||
ghc.ActionRepository = fmt.Sprintf("%s/%s", remoteAction.Org, remoteAction.Repo)
|
||||
ghc.ActionRef = remoteAction.Ref
|
||||
func (sar *stepActionRemote) actionIdentity() (string, string) {
|
||||
if sar.remoteAction == nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return ghc
|
||||
return sar.remoteAction.Org + "/" + sar.remoteAction.Repo, sar.remoteAction.Ref
|
||||
}
|
||||
|
||||
func (sar *stepActionRemote) getStepModel() *model.Step {
|
||||
@@ -205,7 +199,7 @@ func (sar *stepActionRemote) getEnv() *map[string]string {
|
||||
func (sar *stepActionRemote) getIfExpression(ctx context.Context, stage stepStage) string {
|
||||
switch stage {
|
||||
case stepStagePre:
|
||||
github := sar.getGithubContext(ctx)
|
||||
github := getStepGithubContext(ctx, sar)
|
||||
if sar.skipsLocalCheckout(sar.remoteAction, github) {
|
||||
// skip local checkout pre step
|
||||
return "false"
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestStepActionRemote(t *testing.T) {
|
||||
status model.StepStatus
|
||||
runError error
|
||||
}{
|
||||
{name: "run-successful", status: model.StepStatusSuccess},
|
||||
{name: "run-successful", condition: "github.action_repository == 'other/composite' && github.action_ref == 'v2'", status: model.StepStatusSuccess},
|
||||
{name: "run-skipped", condition: "false", status: model.StepStatusSkipped},
|
||||
{name: "run-error", status: model.StepStatusFailure, runError: assert.AnError},
|
||||
} {
|
||||
@@ -110,6 +110,7 @@ func TestStepActionRemote(t *testing.T) {
|
||||
},
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
JobContainer: cm,
|
||||
Env: map[string]string{"GITHUB_ACTION_REPOSITORY": "other/composite", "GITHUB_ACTION_REF": "v2"},
|
||||
},
|
||||
Step: &model.Step{ID: "step", Uses: "remote/action@v1", If: yaml.Node{Value: tt.condition}},
|
||||
readAction: sarm.readAction,
|
||||
@@ -127,6 +128,8 @@ func TestStepActionRemote(t *testing.T) {
|
||||
require.NoError(t, sar.pre()(ctx))
|
||||
assert.Equal(t, tt.runError, sar.main()(ctx))
|
||||
assert.True(t, clonedAction)
|
||||
assert.Equal(t, "remote/action", sar.env["GITHUB_ACTION_REPOSITORY"])
|
||||
assert.Equal(t, "v1", sar.env["GITHUB_ACTION_REF"])
|
||||
assert.Equal(t, &model.StepResult{Conclusion: tt.status, Outcome: tt.status, Outputs: map[string]string{}}, sar.RunContext.StepResults["step"])
|
||||
|
||||
sarm.AssertExpectations(t)
|
||||
@@ -182,7 +185,7 @@ func TestStepActionRemotePrepare(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
root, err := prepare(job, "org/repo@v1")
|
||||
require.NoError(t, err)
|
||||
nested, err := prepare(&RunContext{Config: job.Config, Run: job.Run, JobContainer: job.JobContainer, Parent: job}, "org/repo/save@v1")
|
||||
nested, err := prepare(&RunContext{Config: job.Config, Run: job.Run, JobContainer: job.JobContainer, Parent: job, Env: map[string]string{"GITHUB_ACTION_REPOSITORY": "org/repo", "GITHUB_ACTION_REF": "v1"}}, "$/save")
|
||||
require.NoError(t, err)
|
||||
other, err := prepare(job, "org/repo@v2")
|
||||
require.NoError(t, err)
|
||||
@@ -346,9 +349,6 @@ func Test_newRemoteActionSelfRepo(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{name: "unknown instance", action: "$/.gitea/actions/build", github: &model.GithubContext{Repository: "owner/workflow-repo", Sha: "abc123"}},
|
||||
{name: "empty path", action: "$/", github: workflow},
|
||||
{name: "ref suffix is not allowed", action: "$/.gitea/actions/build@v1", github: workflow},
|
||||
{name: "path traversal", action: "$/../escape", github: workflow},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -35,10 +35,6 @@ func (sd *stepDocker) getRunContext() *RunContext {
|
||||
return sd.RunContext
|
||||
}
|
||||
|
||||
func (sd *stepDocker) getGithubContext(ctx context.Context) *model.GithubContext {
|
||||
return sd.getRunContext().getGithubContext(ctx)
|
||||
}
|
||||
|
||||
func (sd *stepDocker) getStepModel() *model.Step {
|
||||
return sd.Step
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func TestStepDockerMain(t *testing.T) {
|
||||
@@ -57,13 +58,16 @@ func TestStepDockerMain(t *testing.T) {
|
||||
},
|
||||
},
|
||||
JobContainer: cm,
|
||||
Env: map[string]string{"GITHUB_ACTION_REPOSITORY": "other/composite", "GITHUB_ACTION_REF": "v1"},
|
||||
},
|
||||
Step: &model.Step{
|
||||
ID: "1",
|
||||
Uses: "docker://node:14",
|
||||
WorkingDirectory: "workdir",
|
||||
If: yaml.Node{Value: "github.action_repository == 'other/composite' && github.action_ref == 'v1'"},
|
||||
},
|
||||
}
|
||||
require.NoError(t, sd.Step.RawWith.Encode(map[string]string{"args": "identity:${{ github.action_repository }}:${{ github.action_ref }}"}))
|
||||
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
|
||||
|
||||
cm.On("Pull", false).Return(noopExecutor)
|
||||
@@ -80,6 +84,11 @@ func TestStepDockerMain(t *testing.T) {
|
||||
require.NoError(t, sd.main()(ctx))
|
||||
|
||||
assert.Equal(t, "node:14", input.Image)
|
||||
assert.Equal(t, []string{"identity::"}, input.Cmd)
|
||||
assert.Contains(t, input.Env, "GITHUB_ACTION_REPOSITORY=")
|
||||
assert.Contains(t, input.Env, "GITHUB_ACTION_REF=")
|
||||
assert.Equal(t, "other/composite", sd.RunContext.Env["GITHUB_ACTION_REPOSITORY"])
|
||||
assert.Equal(t, "v1", sd.RunContext.Env["GITHUB_ACTION_REF"])
|
||||
|
||||
assert.Empty(t, input.Username)
|
||||
assert.Empty(t, input.Password)
|
||||
|
||||
@@ -222,10 +222,6 @@ func (sr *stepRun) getRunContext() *RunContext {
|
||||
return sr.RunContext
|
||||
}
|
||||
|
||||
func (sr *stepRun) getGithubContext(ctx context.Context) *model.GithubContext {
|
||||
return sr.getRunContext().getGithubContext(ctx)
|
||||
}
|
||||
|
||||
func (sr *stepRun) getStepModel() *model.Step {
|
||||
return sr.Step
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type shellContainerMock struct{ *containerMock }
|
||||
@@ -36,7 +37,6 @@ func TestStepRun(t *testing.T) {
|
||||
sr := &stepRun{
|
||||
RunContext: &RunContext{
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
ExprEval: &expressionEvaluator{},
|
||||
Config: &Config{Env: testCheckoutEnv()},
|
||||
Run: &model.Run{
|
||||
JobID: "1",
|
||||
@@ -48,19 +48,27 @@ func TestStepRun(t *testing.T) {
|
||||
},
|
||||
JobContainer: cm,
|
||||
jobRunDefaults: model.RunDefaults{Shell: "bash"},
|
||||
Env: map[string]string{"GITHUB_ACTION_REPOSITORY": "other/composite", "GITHUB_ACTION_REF": "v1"},
|
||||
},
|
||||
Step: &model.Step{
|
||||
ID: "1",
|
||||
Run: "cmd",
|
||||
Run: "cmd${{ github.action_repository }}${{ github.action_ref }}",
|
||||
WorkingDirectory: "workdir",
|
||||
If: yaml.Node{Value: "github.action_repository == 'other/composite' && github.action_ref == 'v1'"},
|
||||
},
|
||||
}
|
||||
sr.RunContext.ExprEval = sr.RunContext.NewExpressionEvaluator(t.Context())
|
||||
require.NoError(t, sr.Step.Env.Encode(map[string]string{"GITHUB_ACTION_REPOSITORY": "forged/repo", "GITHUB_ACTION_REF": "forged-ref"}))
|
||||
|
||||
cm.On("Copy", "/var/run/act", []*container.FileEntry{fileEntry}).Return(noopExecutor)
|
||||
cm.On("Exec", []string{"bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "/var/run/act/workflow/1.sh"}, mock.AnythingOfType("map[string]string"), "", "workdir").Return(noopExecutor)
|
||||
|
||||
expectStepFileCommands(t, cm)
|
||||
require.NoError(t, sr.main()(t.Context()))
|
||||
assert.Empty(t, sr.env["GITHUB_ACTION_REPOSITORY"])
|
||||
assert.Empty(t, sr.env["GITHUB_ACTION_REF"])
|
||||
assert.Equal(t, "other/composite", sr.RunContext.Env["GITHUB_ACTION_REPOSITORY"])
|
||||
assert.Equal(t, "v1", sr.RunContext.Env["GITHUB_ACTION_REF"])
|
||||
|
||||
cm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ toolchain go1.27.3
|
||||
|
||||
require (
|
||||
connectrpc.com/connect/v2 v2.0.1
|
||||
gitea.dev/actionslib v1.5.0
|
||||
gitea.dev/actionslib v1.5.1
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.3
|
||||
github.com/coder/websocket v1.8.15
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
@@ -33,7 +33,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.47.0
|
||||
go.opentelemetry.io/proto/otlp v1.11.1
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525
|
||||
golang.org/x/net v0.61.0
|
||||
golang.org/x/net v0.62.0
|
||||
golang.org/x/sync v0.24.0
|
||||
golang.org/x/sys v0.49.0
|
||||
golang.org/x/term v0.47.0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
connectrpc.com/connect/v2 v2.0.1 h1:WTAycxe87UqxSM5oxS30d1dalZ+C5xz8Ec8O6zVYkHQ=
|
||||
connectrpc.com/connect/v2 v2.0.1/go.mod h1:N32OLrstcPUzkykFynnefFro3+xzSdOrZlK2jHmS/xo=
|
||||
gitea.dev/actionslib v1.5.0 h1:dC3v2HTWbbvyNBSdg2W3kd4WNC/p+FnZaKpl1MdzM00=
|
||||
gitea.dev/actionslib v1.5.0/go.mod h1:C9wmOFgDBCXyRvg4A3KTX8Vszs8v9+elH4QLGVDolqU=
|
||||
gitea.dev/actionslib v1.5.1 h1:QVOaENNWx4EFRlZvrWmMTKaKLbaqxdlhWLtsI4kf2v8=
|
||||
gitea.dev/actionslib v1.5.1/go.mod h1:C9wmOFgDBCXyRvg4A3KTX8Vszs8v9+elH4QLGVDolqU=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -111,8 +111,8 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525 h1:FtzHH3s777Nx50YcPAcodAPm1mNakc228nMYbr3f+90=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
|
||||
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
|
||||
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
|
||||
@@ -104,7 +104,7 @@ func TestTreeKillCancelBeforeCaptureKillsTree(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = stdin.Close() })
|
||||
t.Cleanup(func() { _ = input.Close() })
|
||||
cmd := exec.CommandContext(t.Context(), "/bin/sh", "-c", "cat <&0 & echo $!; read value")
|
||||
cmd := exec.CommandContext(t.Context(), "/bin/sh", "-c", "exec 3<&0; cat <&3 & echo $!; read value")
|
||||
cmd.Stdin = stdin
|
||||
cmd.SysProcAttr = SysProcAttr("", false)
|
||||
output, err := cmd.StdoutPipe()
|
||||
|
||||
Reference in new issue
Block a user