mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-03-20 03:46:09 +08:00
251 lines
5.3 KiB
Go
251 lines
5.3 KiB
Go
package tart
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"gitea.com/gitea/act_runner/pkg/common"
|
|
"github.com/avast/retry-go"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
const tartCommandName = "tart"
|
|
|
|
var (
|
|
ErrTartNotFound = errors.New("tart command not found")
|
|
ErrTartFailed = errors.New("tart command returned non-zero exit code")
|
|
ErrVMFailed = errors.New("VM errored")
|
|
)
|
|
|
|
type VM struct {
|
|
id string
|
|
runcmd *exec.Cmd
|
|
}
|
|
|
|
func ExistingVM(actEnv Env) *VM {
|
|
return &VM{
|
|
id: actEnv.VirtualMachineID(),
|
|
}
|
|
}
|
|
|
|
func CreateNewVM(
|
|
ctx context.Context,
|
|
actEnv Env,
|
|
cpuOverride uint64,
|
|
memoryOverride uint64,
|
|
) (*VM, error) {
|
|
common.Logger(ctx).Debug("CreateNewVM")
|
|
vm := &VM{
|
|
id: actEnv.VirtualMachineID(),
|
|
}
|
|
|
|
if err := vm.cloneAndConfigure(ctx, actEnv, cpuOverride, memoryOverride); err != nil {
|
|
return nil, fmt.Errorf("failed to clone the VM: %w", err)
|
|
}
|
|
|
|
return vm, nil
|
|
}
|
|
|
|
func (vm *VM) cloneAndConfigure(
|
|
ctx context.Context,
|
|
actEnv Env,
|
|
cpuOverride uint64,
|
|
memoryOverride uint64,
|
|
) error {
|
|
_, _, err := Exec(ctx, "clone", actEnv.JobImage, vm.id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cpuOverride != 0 {
|
|
_, _, err = Exec(ctx, "set", "--cpu", strconv.FormatUint(cpuOverride, 10), vm.id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if memoryOverride != 0 {
|
|
_, _, err = Exec(ctx, "set", "--memory", strconv.FormatUint(memoryOverride, 10), vm.id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (vm *VM) Start(ctx context.Context, config Config, _ *Env, customDirectoryMounts []string) error {
|
|
os.Remove(vm.tartRunOutputPath())
|
|
var runArgs = []string{"run"}
|
|
|
|
if config.Softnet {
|
|
runArgs = append(runArgs, "--net-softnet")
|
|
}
|
|
|
|
if config.Headless {
|
|
runArgs = append(runArgs, "--no-graphics")
|
|
}
|
|
|
|
for _, customDirectoryMount := range customDirectoryMounts {
|
|
runArgs = append(runArgs, "--dir", customDirectoryMount)
|
|
}
|
|
|
|
runArgs = append(runArgs, vm.id)
|
|
|
|
// Use Background context, because we want to keep the VM running
|
|
cmd := exec.CommandContext(context.Background(), tartCommandName, runArgs...)
|
|
|
|
common.Logger(ctx).Debug(strings.Join(runArgs, " "))
|
|
|
|
cmd.Stdout = config.Writer
|
|
cmd.Stderr = config.Writer
|
|
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
Setsid: true,
|
|
}
|
|
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
vm.runcmd = cmd
|
|
return nil
|
|
}
|
|
|
|
func (vm *VM) OpenSSH(ctx context.Context, config Config) (*ssh.Client, error) {
|
|
ip, err := vm.IP(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addr := ip + ":22"
|
|
|
|
var netConn net.Conn
|
|
if err := retry.Do(func() error {
|
|
dialer := net.Dialer{}
|
|
|
|
netConn, err = dialer.DialContext(ctx, "tcp", addr)
|
|
|
|
return err
|
|
}, retry.Context(ctx)); err != nil {
|
|
return nil, fmt.Errorf("%w: failed to connect via SSH: %v", ErrVMFailed, err)
|
|
}
|
|
|
|
sshConfig := &ssh.ClientConfig{
|
|
HostKeyCallback: func(_ string, _ net.Addr, _ ssh.PublicKey) error {
|
|
return nil
|
|
},
|
|
User: config.SSHUsername,
|
|
Auth: []ssh.AuthMethod{
|
|
ssh.Password(config.SSHPassword),
|
|
},
|
|
}
|
|
|
|
sshConn, chans, reqs, err := ssh.NewClientConn(netConn, addr, sshConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: failed to connect via SSH: %v", ErrVMFailed, err)
|
|
}
|
|
|
|
return ssh.NewClient(sshConn, chans, reqs), nil
|
|
}
|
|
|
|
func (vm *VM) IP(ctx context.Context) (string, error) {
|
|
stdout, _, err := Exec(ctx, "ip", "--wait", "60", vm.id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return strings.TrimSpace(stdout), nil
|
|
}
|
|
|
|
func (vm *VM) Stop(ctx context.Context) error {
|
|
common.Logger(ctx).Debug("Stop VM REAL?")
|
|
if vm.runcmd != nil {
|
|
common.Logger(ctx).Debug("send sigint")
|
|
_ = vm.runcmd.Process.Signal(os.Interrupt)
|
|
common.Logger(ctx).Debug("wait for cmd")
|
|
_ = vm.runcmd.Wait()
|
|
common.Logger(ctx).Debug("cmd stopped")
|
|
return nil
|
|
}
|
|
_, _, err := Exec(ctx, "stop", vm.id)
|
|
return err
|
|
}
|
|
|
|
func (vm *VM) Delete(ctx context.Context) error {
|
|
_, _, err := Exec(ctx, "delete", vm.id)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: failed to delete VM %s: %v", ErrVMFailed, vm.id, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func Exec(
|
|
ctx context.Context,
|
|
args ...string,
|
|
) (string, string, error) {
|
|
return ExecWithEnv(ctx, nil, args...)
|
|
}
|
|
|
|
func ExecWithEnv(
|
|
ctx context.Context,
|
|
env map[string]string,
|
|
args ...string,
|
|
) (string, string, error) {
|
|
cmd := exec.CommandContext(ctx, tartCommandName, args...)
|
|
|
|
// Base environment
|
|
cmd.Env = cmd.Environ()
|
|
|
|
// Environment overrides
|
|
for key, value := range env {
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
|
|
}
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
if errors.Is(err, exec.ErrNotFound) {
|
|
return "", "", fmt.Errorf("%w: %s command not found in PATH, make sure Tart is installed",
|
|
ErrTartNotFound, tartCommandName)
|
|
}
|
|
|
|
if _, ok := err.(*exec.ExitError); ok {
|
|
// Tart command failed, redefine the error
|
|
// to be the Tart-specific output
|
|
err = fmt.Errorf("%w: %q", ErrTartFailed, firstNonEmptyLine(stderr.String(), stdout.String()))
|
|
}
|
|
}
|
|
|
|
return stdout.String(), stderr.String(), err
|
|
}
|
|
|
|
func firstNonEmptyLine(outputs ...string) string {
|
|
for _, output := range outputs {
|
|
for _, line := range strings.Split(output, "\n") {
|
|
if line != "" {
|
|
return line
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (vm *VM) tartRunOutputPath() string {
|
|
return filepath.Join(os.TempDir(), fmt.Sprintf("%s-tart-run-output.log", vm.id))
|
|
}
|