mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-11 10:24:09 +00:00
refactor!: remove the builtin actions (#1407)
Removes `builtin:checkout`, `builtin:upload-artifact` and `builtin:download-artifact`, along with the container helpers only they used. Builtin actions are tied to the runner version, so users cannot pin them, and they diverge from the shareable actions ecosystem that tools like zizmor build on. Workflows should use `actions/checkout`, `actions/upload-artifact` and `actions/download-artifact`, or lightweight third-party actions such as https://github.com/taiki-e/checkout-action. **Breaking:** steps with `uses: builtin:checkout`, released in v4.0.0, now fail and need to switch to one of the actions above. The artifact actions were never released. Docs: https://gitea.com/gitea/docs/pulls/643 Fixes: https://gitea.com/gitea/runner/issues/1405 Co-authored-by: silverwind <[email protected]> Reviewed-on: https://gitea.com/gitea/runner/pulls/1407 Reviewed-by: silverwind <[email protected]> Co-authored-by: bircni <[email protected]>
This commit is contained in:
1 parent
85d007c47f
commit
c59caa9689
54 files changed
+18
-5751
No files matched your search
@@ -5,7 +5,6 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -35,38 +34,6 @@ func (e ExitCodeError) Error() string {
|
||||
return fmt.Sprintf("Process completed with exit code %d.", int(e))
|
||||
}
|
||||
|
||||
type commandOutput struct {
|
||||
*io.PipeReader
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
err error
|
||||
}
|
||||
|
||||
func (output *commandOutput) Close() error {
|
||||
_, _ = io.Copy(io.Discard, output.PipeReader)
|
||||
output.PipeReader.Close()
|
||||
output.cancel()
|
||||
<-output.done
|
||||
return output.err
|
||||
}
|
||||
|
||||
func streamOutput(ctx context.Context, run func(context.Context, io.Writer, io.Writer) error) io.ReadCloser {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
reader, writer := io.Pipe()
|
||||
output := &commandOutput{PipeReader: reader, cancel: cancel, done: make(chan struct{})}
|
||||
go func() {
|
||||
defer close(output.done)
|
||||
defer cancel()
|
||||
var stderr bytes.Buffer
|
||||
output.err = run(ctx, writer, &stderr)
|
||||
if output.err != nil && strings.TrimSpace(stderr.String()) != "" {
|
||||
output.err = fmt.Errorf("%w: %s", output.err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
writer.CloseWithError(output.err)
|
||||
}()
|
||||
return output
|
||||
}
|
||||
|
||||
// NewContainerInput the input for the New function
|
||||
type NewContainerInput struct {
|
||||
Image string
|
||||
@@ -167,9 +134,7 @@ type Container interface {
|
||||
Create(capAdd, capDrop []string) common.Executor
|
||||
Copy(destPath string, files ...*FileEntry) common.Executor
|
||||
CopyDir(destPath, srcPath string, useGitIgnore, skipGitDir bool) common.Executor
|
||||
ExecOutput(ctx context.Context, command []string) (io.ReadCloser, error)
|
||||
GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error)
|
||||
CopyTarStream(ctx context.Context, destPath string, write func(*TarWriter) error) error
|
||||
Inspect(ctx context.Context) (*Info, error)
|
||||
DumpLogs(ctx context.Context) error
|
||||
Pull(forcePull bool) common.Executor
|
||||
|
||||
@@ -139,47 +139,6 @@ func (cr *containerReference) CopyDir(destPath, srcPath string, useGitIgnore, sk
|
||||
).IfNot(common.Dryrun)
|
||||
}
|
||||
|
||||
func (cr *containerReference) ExecOutput(ctx context.Context, command []string) (io.ReadCloser, error) {
|
||||
if common.Dryrun(ctx) {
|
||||
return nil, errors.New("DRYRUN is not supported in ExecOutput")
|
||||
}
|
||||
if err := cr.connect()(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cr.find()(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cr.id == "" {
|
||||
return nil, cr.missingContainerError("exec %v", command)
|
||||
}
|
||||
return streamOutput(ctx, func(ctx context.Context, stdout, stderr io.Writer) error {
|
||||
created, err := cr.cli.ExecCreate(ctx, cr.id, client.ExecCreateOptions{
|
||||
Cmd: command, AttachStdout: true, AttachStderr: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attached, err := cr.cli.ExecAttach(ctx, created.ID, client.ExecAttachOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer attached.Close()
|
||||
stop := context.AfterFunc(ctx, func() { attached.Close() })
|
||||
defer stop()
|
||||
if _, err := stdcopy.StdCopy(stdout, stderr, attached.Reader); err != nil {
|
||||
return err
|
||||
}
|
||||
inspected, err := cr.cli.ExecInspect(ctx, created.ID, client.ExecInspectOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inspected.ExitCode != 0 {
|
||||
return ExitCodeError(inspected.ExitCode)
|
||||
}
|
||||
return nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (cr *containerReference) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
|
||||
if common.Dryrun(ctx) {
|
||||
return nil, errors.New("DRYRUN is not supported in GetContainerArchive")
|
||||
@@ -993,37 +952,6 @@ func (cr *containerReference) extractionRoot(dstPath string) string {
|
||||
return "/"
|
||||
}
|
||||
|
||||
func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string, write func(*TarWriter) error) error {
|
||||
if common.Dryrun(ctx) {
|
||||
return nil
|
||||
}
|
||||
if err := cr.connect()(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cr.find()(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if cr.id == "" {
|
||||
return cr.missingContainerError("copy to %s", destPath)
|
||||
}
|
||||
destPath = path.Clean(destPath)
|
||||
root := cr.extractionRoot(destPath)
|
||||
rel := func(fullPath string) string { return strings.TrimPrefix(strings.TrimPrefix(fullPath, root), "/") }
|
||||
var parents []string // sent as entries so they belong to the job user, the daemon creates missing ones as root
|
||||
for dir := destPath; dir != root && dir != "/" && dir != "."; dir = path.Dir(dir) {
|
||||
if _, err := cr.cli.ContainerStatPath(ctx, cr.id, client.ContainerStatPathOptions{Path: dir}); err == nil {
|
||||
break
|
||||
} else if !cerrdefs.IsNotFound(err) {
|
||||
return fmt.Errorf("stat %s: %w", dir, err)
|
||||
}
|
||||
parents = append(parents, rel(dir))
|
||||
}
|
||||
slices.Reverse(parents)
|
||||
content := pipeTar(cr.UID, cr.GID, rel(destPath), parents, write)
|
||||
defer content.Close()
|
||||
return cr.upload(ctx, root, content)
|
||||
}
|
||||
|
||||
func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if cr.id == "" {
|
||||
|
||||
@@ -260,28 +260,6 @@ func TestDockerExecFailure(t *testing.T) {
|
||||
assert.Equal(t, ExitCodeError(1), exitErr)
|
||||
assert.Equal(t, "Process completed with exit code 1.", err.Error())
|
||||
|
||||
client.On("ContainerInspect", ctx, "123", mobyclient.ContainerInspectOptions{}).
|
||||
Return(mobyclient.ContainerInspectResult{}, nil)
|
||||
client.On("ExecCreate", mock.Anything, "123", mobyclient.ExecCreateOptions{
|
||||
Cmd: []string{"printf", `a\b`}, AttachStdout: true, AttachStderr: true,
|
||||
}).Return(mobyclient.ExecCreateResult{ID: "output"}, nil)
|
||||
client.On("ExecAttach", mock.Anything, "output", mobyclient.ExecAttachOptions{}).
|
||||
Return(mobyclient.ExecAttachResult{
|
||||
Conn: &mockConn{},
|
||||
Reader: bufio.NewReader(bytes.NewReader(append(stdcopyFrame(stdcopy.Stdout, "a\x00b"), stdcopyFrame(stdcopy.Stderr, " failed \n")...))),
|
||||
}, nil)
|
||||
client.On("ExecInspect", mock.Anything, "output", mobyclient.ExecInspectOptions{}).
|
||||
Return(mobyclient.ExecInspectResult{ExitCode: 3}, nil)
|
||||
output, err := cr.ExecOutput(ctx, []string{"printf", `a\b`})
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(output)
|
||||
require.ErrorAs(t, err, &exitErr)
|
||||
assert.Equal(t, ExitCodeError(3), exitErr)
|
||||
assert.Equal(t, "Process completed with exit code 3.: failed", err.Error())
|
||||
assert.Equal(t, "a\x00b", string(content))
|
||||
require.ErrorAs(t, output.Close(), &exitErr)
|
||||
assert.Equal(t, ExitCodeError(3), exitErr)
|
||||
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -570,8 +548,6 @@ func TestRejectsMissingContainer(t *testing.T) {
|
||||
check("exec", cr.exec([]string{"echo"}, nil, "", "")(ctx))
|
||||
_, err := cr.GetContainerArchive(ctx, "/var/run/act/x")
|
||||
check("GetContainerArchive", err)
|
||||
_, err = cr.ExecOutput(ctx, []string{"true"})
|
||||
check("ExecOutput", err)
|
||||
_, err = cr.Inspect(ctx)
|
||||
check("Inspect", err)
|
||||
|
||||
@@ -583,46 +559,6 @@ func TestRejectsMissingContainer(t *testing.T) {
|
||||
check("Inspect after removal", err)
|
||||
}
|
||||
|
||||
func TestCopyTarStreamOwnsMissingParentsAndRejectsEscapes(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
client := &mockDockerClient{}
|
||||
client.On("ContainerInspect", ctx, "job", mobyclient.ContainerInspectOptions{}).
|
||||
Return(mobyclient.ContainerInspectResult{Container: container.InspectResponse{ID: "job", Name: "/job"}}, nil)
|
||||
stat := func(p string, err error) {
|
||||
client.On("ContainerStatPath", ctx, "job", mobyclient.ContainerStatPathOptions{Path: p}).
|
||||
Return(mobyclient.ContainerStatPathResult{}, err).Once()
|
||||
}
|
||||
stat("/w/out/a", cerrdefs.ErrNotFound)
|
||||
stat("/w/out", cerrdefs.ErrNotFound)
|
||||
stat("/w", nil)
|
||||
var entries []string
|
||||
client.On("CopyToContainer", ctx, "job", mock.Anything).Run(func(args mock.Arguments) {
|
||||
opts := args.Get(2).(mobyclient.CopyToContainerOptions)
|
||||
assert.Equal(t, "/", opts.DestinationPath)
|
||||
reader := tar.NewReader(opts.Content)
|
||||
for {
|
||||
header, err := reader.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []any{1001, 1002, ""}, []any{header.Uid, header.Gid, header.Uname}, header.Name)
|
||||
entries = append(entries, header.Name)
|
||||
}
|
||||
}).Return(mobyclient.CopyToContainerResult{}, nil).Once()
|
||||
|
||||
cr := &containerReference{id: "job", cli: client, input: &NewContainerInput{Name: "job"}, UID: 1001, GID: 1002}
|
||||
require.NoError(t, cr.CopyTarStream(ctx, "/w/out/a/", func(tw *TarWriter) error {
|
||||
return tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: "dir/f.txt", Mode: 0o644, Uname: "nobody"})
|
||||
}))
|
||||
assert.Equal(t, []string{"w/out/", "w/out/a/", "w/out/a/dir/", "w/out/a/dir/f.txt"}, entries)
|
||||
client.AssertExpectations(t)
|
||||
|
||||
require.ErrorContains(t, writeTarStream(io.Discard, 0, 0, "w/out/a", nil, func(tw *TarWriter) error {
|
||||
return tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: "../../../etc/passwd"})
|
||||
}), "escapes")
|
||||
}
|
||||
|
||||
func TestPublicCopyPipelineHandlesStaleID(t *testing.T) {
|
||||
source := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(source, "x"), nil, 0o644))
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -116,20 +115,6 @@ func (e *HostEnvironment) Inspect(_ context.Context) (*Info, error) {
|
||||
return &Info{Health: HealthNone, Ports: map[string]string{}}, nil
|
||||
}
|
||||
|
||||
func (e *HostEnvironment) ExecOutput(ctx context.Context, command []string) (io.ReadCloser, error) {
|
||||
return streamOutput(ctx, func(ctx context.Context, stdout, stderr io.Writer) error {
|
||||
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
|
||||
cmd.Dir, cmd.Stdout, cmd.Stderr = e.Workdir, stdout, stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exit, ok := errors.AsType[*exec.ExitError](err); ok {
|
||||
return ExitCodeError(exit.ExitCode())
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (e *HostEnvironment) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
|
||||
srcPath = filepath.Clean(srcPath)
|
||||
fi, err := os.Lstat(srcPath)
|
||||
@@ -177,58 +162,6 @@ func writeHostArchive(ctx context.Context, tc *filecollector.TarCollector, srcPa
|
||||
return tc.WriteFile(fi.Name(), fi, linkname, f)
|
||||
}
|
||||
|
||||
func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, write func(*TarWriter) error) error {
|
||||
if err := os.MkdirAll(destPath, 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := os.OpenRoot(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer root.Close()
|
||||
content := pipeTar(0, 0, "", nil, write)
|
||||
defer content.Close()
|
||||
tr := tar.NewReader(content)
|
||||
var dirs []*tar.Header
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
hdr, err := tr.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
hdr.Name = filepath.FromSlash(strings.TrimSuffix(hdr.Name, "/"))
|
||||
if hdr.Typeflag == tar.TypeDir {
|
||||
err = root.MkdirAll(hdr.Name, 0o777) // modes apply after the contents, so read-only directories can be filled
|
||||
dirs = append(dirs, hdr)
|
||||
} else {
|
||||
err = writeRootFile(root, hdr.Name, hdr.FileInfo().Mode().Perm(), tr)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, dir := range slices.Backward(dirs) {
|
||||
if err := root.Chmod(dir.Name, dir.FileInfo().Mode().Perm()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeRootFile(root *os.Root, name string, mode fs.FileMode, body io.Reader) error {
|
||||
_ = root.Remove(name) // replaces read-only files and applies mode, as the daemon and tar do
|
||||
file, err := root.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(file, body)
|
||||
return errors.Join(err, file.Close())
|
||||
}
|
||||
|
||||
func (e *HostEnvironment) Pull(_ bool) common.Executor {
|
||||
return common.NewPipelineExecutor()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -84,69 +83,11 @@ func TestGetContainerArchive(t *testing.T) {
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
}
|
||||
|
||||
func TestHostCopyTarStream(t *testing.T) {
|
||||
dest := filepath.Join(t.TempDir(), "out")
|
||||
write := func(name string, mode int64, body string) func(*TarWriter) error {
|
||||
return func(tw *TarWriter) error {
|
||||
if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: name, Mode: mode, Size: int64(len(body))}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tw.Write([]byte(body))
|
||||
return err
|
||||
}
|
||||
}
|
||||
e := &HostEnvironment{}
|
||||
require.NoError(t, e.CopyTarStream(t.Context(), dest, write("dir/run.sh", 0o755, "#!/bin/sh\n")))
|
||||
content, err := os.ReadFile(filepath.Join(dest, "dir", "run.sh"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "#!/bin/sh\n", string(content))
|
||||
if runtime.GOOS != "windows" {
|
||||
require.NoError(t, os.Chmod(filepath.Join(dest, "dir", "run.sh"), 0o444))
|
||||
require.NoError(t, e.CopyTarStream(t.Context(), dest, func(tw *TarWriter) error {
|
||||
if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeDir, Name: "dir/", Mode: 0o555}); err != nil {
|
||||
return err
|
||||
}
|
||||
return write("dir/run.sh", 0o755, "")(tw)
|
||||
}))
|
||||
t.Cleanup(func() { require.NoError(t, os.Chmod(filepath.Join(dest, "dir"), 0o755)) })
|
||||
for name, mode := range map[string]os.FileMode{"dir": 0o555, filepath.Join("dir", "run.sh"): 0o755} {
|
||||
info, err := os.Stat(filepath.Join(dest, name))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, mode, info.Mode().Perm(), name)
|
||||
}
|
||||
require.NoError(t, os.Symlink(t.TempDir(), filepath.Join(dest, "link")))
|
||||
require.ErrorContains(t, e.CopyTarStream(t.Context(), dest, write("link/x", 0o644, "x")), "escapes")
|
||||
}
|
||||
|
||||
require.ErrorContains(t, e.CopyTarStream(t.Context(), dest, write("../escape", 0o644, "x")), "escapes")
|
||||
assert.NoFileExists(t, filepath.Join(filepath.Dir(dest), "escape"))
|
||||
require.Error(t, e.CopyTarStream(t.Context(), dest, func(_tw *TarWriter) error { return fmt.Errorf("read body: %w", io.EOF) }))
|
||||
canceled, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
require.ErrorIs(t, e.CopyTarStream(canceled, dest, write("late", 0o644, "x")), context.Canceled)
|
||||
assert.NoFileExists(t, filepath.Join(dest, "late"))
|
||||
}
|
||||
|
||||
func TestHostEnvironmentExecExitCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("uses POSIX shell")
|
||||
}
|
||||
for _, read := range []bool{true, false} {
|
||||
output, err := (&HostEnvironment{AllocatePTY: true}).ExecOutput(t.Context(), []string{"sh", "-c", `test ! -t 1; printf 'out\000'; printf ' failed \n' >&2; exit 3`})
|
||||
require.NoError(t, err)
|
||||
if read {
|
||||
content, err := io.ReadAll(output)
|
||||
var exit ExitCodeError
|
||||
require.ErrorAs(t, err, &exit)
|
||||
assert.Equal(t, ExitCodeError(3), exit)
|
||||
assert.Equal(t, "out\x00", string(content))
|
||||
assert.Equal(t, "Process completed with exit code 3.: failed", err.Error())
|
||||
}
|
||||
var exit ExitCodeError
|
||||
require.ErrorAs(t, output.Close(), &exit)
|
||||
assert.Equal(t, ExitCodeError(3), exit)
|
||||
}
|
||||
for name, allocatePTY := range map[string]bool{"without PTY": false, "with PTY": true} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
+10
-14
@@ -553,10 +553,6 @@ func (c *kubernetesContainer) Copy(destPath string, files ...*FileEntry) common.
|
||||
}
|
||||
}
|
||||
|
||||
func (c *kubernetesContainer) CopyTarStream(ctx context.Context, destPath string, write func(*TarWriter) error) error {
|
||||
return c.extract(ctx, destPath, true, func(writer io.Writer) error { return writeTarStream(writer, 0, 0, "", nil, write) }) // streams carry large content like artifacts
|
||||
}
|
||||
|
||||
func (c *kubernetesContainer) CopyDir(destPath, srcPath string, useGitIgnore, skipGitDir bool) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
return c.extract(ctx, destPath, true, func(writer io.Writer) error { // directories are actions and workspaces, megabytes of compressible sources
|
||||
@@ -565,17 +561,17 @@ func (c *kubernetesContainer) CopyDir(destPath, srcPath string, useGitIgnore, sk
|
||||
}
|
||||
}
|
||||
|
||||
func (c *kubernetesContainer) ExecOutput(ctx context.Context, command []string) (io.ReadCloser, error) {
|
||||
return streamOutput(ctx, func(ctx context.Context, stdout, stderr io.Writer) error {
|
||||
return c.pod.client.exec(ctx, c.pod.name, c.name, command, nil, stdout, stderr)
|
||||
}), nil
|
||||
}
|
||||
|
||||
// GetContainerArchive streams the archive, checking the path first as tar writes an empty archive for a missing one.
|
||||
func (c *kubernetesContainer) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
|
||||
reader, err := c.ExecOutput(ctx, []string{"sh", "-c", `test -e "$1" -o -L "$1" && exec tar -cf - -C "$2" "$3"`, "sh", srcPath, path.Dir(srcPath), path.Base(srcPath)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader, writer := io.Pipe()
|
||||
go func() {
|
||||
var stderr bytes.Buffer
|
||||
err := c.pod.client.exec(ctx, c.pod.name, c.name, []string{"sh", "-c", `test -e "$1" -o -L "$1" && exec tar -cf - -C "$2" "$3"`, "sh", srcPath, path.Dir(srcPath), path.Base(srcPath)}, nil, writer, &stderr)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("archive %s: %w: %s", srcPath, err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
writer.CloseWithError(err)
|
||||
}()
|
||||
return startedArchive(reader)
|
||||
}
|
||||
|
||||
|
||||
@@ -273,17 +273,6 @@ func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
|
||||
assert.Equal(t, "hunter2\nit's\nme\n5\n"+workdir+"\n", jobLog.String())
|
||||
assert.Equal(t, ExitCodeError(3), job.Exec([]string{"sh", "-c", "echo failing >&2; exit 3"}, nil, "", "")(ctx))
|
||||
assert.Contains(t, jobLog.String(), "failing\n")
|
||||
output, err := job.ExecOutput(ctx, []string{"sh", "-c", `printf 'out\000'; printf ' failed \n' >&2; exit 3`})
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(output)
|
||||
var exit ExitCodeError
|
||||
require.ErrorAs(t, err, &exit)
|
||||
assert.Equal(t, ExitCodeError(3), exit)
|
||||
assert.Contains(t, err.Error(), ": failed")
|
||||
assert.Equal(t, "out\x00", string(content))
|
||||
require.ErrorAs(t, output.Close(), &exit)
|
||||
assert.Equal(t, ExitCodeError(3), exit)
|
||||
|
||||
for _, query := range cluster.requests["exec"] {
|
||||
assert.NotContains(t, query, "hunter2")
|
||||
}
|
||||
@@ -296,7 +285,7 @@ func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
|
||||
reader := tar.NewReader(archive)
|
||||
header, err := reader.Next()
|
||||
require.NoError(t, err)
|
||||
content, err = io.ReadAll(reader)
|
||||
content, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "event.json", header.Name)
|
||||
assert.JSONEq(t, `{"ref":"main"}`, string(content))
|
||||
@@ -320,18 +309,6 @@ func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
|
||||
assert.Contains(t, lastExec(), "command="+url.QueryEscape(workdir+"/act/actions/sha/"))
|
||||
assert.Contains(t, lastExec(), url.QueryEscape("gzip -dc |"))
|
||||
|
||||
require.NoError(t, job.CopyTarStream(ctx, workdir+"/download/new", func(tw *TarWriter) error {
|
||||
if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: "dir/data.txt", Mode: 0o644, Size: 7}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tw.Write([]byte("payload"))
|
||||
return err
|
||||
}))
|
||||
downloaded, err := os.ReadFile(workdir + "/download/new/dir/data.txt")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "payload", string(downloaded))
|
||||
assert.Contains(t, lastExec(), url.QueryEscape("gzip -dc |"))
|
||||
|
||||
pathEnv := map[string]string{}
|
||||
require.NoError(t, job.UpdateFromImageEnv(&pathEnv)(ctx))
|
||||
assert.Equal(t, path, pathEnv["PATH"])
|
||||
|
||||
+1
-70
@@ -8,11 +8,7 @@ import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -20,73 +16,8 @@ import (
|
||||
"gitea.com/gitea/runner/act/filecollector"
|
||||
)
|
||||
|
||||
// TarWriter takes directories and files named relative to the destination, owned by the job user.
|
||||
type TarWriter struct {
|
||||
writer *tar.Writer
|
||||
uid, gid int
|
||||
prefix string
|
||||
dirs map[string]bool
|
||||
}
|
||||
|
||||
func (w *TarWriter) Write(data []byte) (int, error) {
|
||||
return w.writer.Write(data)
|
||||
}
|
||||
|
||||
func (w *TarWriter) WriteHeader(hdr *tar.Header) error {
|
||||
name := strings.TrimSuffix(hdr.Name, "/")
|
||||
if !fs.ValidPath(name) {
|
||||
return fmt.Errorf("tar entry %q escapes the destination", hdr.Name)
|
||||
}
|
||||
if hdr.Typeflag != tar.TypeDir && hdr.Typeflag != tar.TypeReg {
|
||||
return fmt.Errorf("tar entry %q: unsupported type %q", hdr.Name, hdr.Typeflag)
|
||||
}
|
||||
for index := range len(name) {
|
||||
if dir := name[:index]; name[index] == '/' && !w.dirs[dir] {
|
||||
if err := w.write(&tar.Header{Typeflag: tar.TypeDir, Name: path.Join(w.prefix, dir), Mode: 0o755}); err != nil {
|
||||
return err
|
||||
}
|
||||
w.dirs[dir] = true
|
||||
}
|
||||
}
|
||||
if hdr.Typeflag == tar.TypeDir {
|
||||
w.dirs[name] = true
|
||||
}
|
||||
entry := *hdr
|
||||
entry.Name = path.Join(w.prefix, name)
|
||||
return w.write(&entry)
|
||||
}
|
||||
|
||||
func (w *TarWriter) write(hdr *tar.Header) error {
|
||||
hdr.Uid, hdr.Gid, hdr.Uname, hdr.Gname = w.uid, w.gid, "", "" // tar running as root prefers names over ids
|
||||
if hdr.Typeflag == tar.TypeDir {
|
||||
hdr.Name += "/"
|
||||
}
|
||||
return w.writer.WriteHeader(hdr)
|
||||
}
|
||||
|
||||
func writeTarStream(dst io.Writer, uid, gid int, prefix string, parents []string, write func(*TarWriter) error) error {
|
||||
tw := &TarWriter{writer: tar.NewWriter(dst), uid: uid, gid: gid, prefix: prefix, dirs: map[string]bool{}}
|
||||
for _, name := range parents {
|
||||
if err := tw.write(&tar.Header{Typeflag: tar.TypeDir, Name: name, Mode: 0o755}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := write(tw); errors.Is(err, io.EOF) {
|
||||
return fmt.Errorf("tar stream: %v", err) // %v, as a wrapped io.EOF reads as the archive's end
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
return tw.writer.Close()
|
||||
}
|
||||
|
||||
func pipeTar(uid, gid int, prefix string, parents []string, write func(*TarWriter) error) *io.PipeReader {
|
||||
reader, writer := io.Pipe()
|
||||
go func() { writer.CloseWithError(writeTarStream(writer, uid, gid, prefix, parents, write)) }()
|
||||
return reader
|
||||
}
|
||||
|
||||
// startedArchive returns reader once its first byte arrives, so failing to start archiving errors here, not on read.
|
||||
func startedArchive(reader io.ReadCloser) (io.ReadCloser, error) {
|
||||
func startedArchive(reader *io.PipeReader) (io.ReadCloser, error) {
|
||||
buffered := bufio.NewReader(reader)
|
||||
if _, err := buffered.Peek(1); err != nil {
|
||||
reader.Close()
|
||||
|
||||
Reference in new issue
Block a user