package pipeline_executor import ( "bytes" "context" "fmt" "io" "os" "os/exec" "path/filepath" "git.ohea.xyz/cursorius/server/config" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/op/go-logging" ) var log = logging.MustGetLogger("cursorius-server") type PipelineExecution struct { Name string Job config.Job Ref string } func ExecutePipeline(pe PipelineExecution, pipelineConf config.PipelineConf) error { jobFolder := filepath.Join(pipelineConf.WorkingDir, pe.Name) log.Debugf("Job %v configured with URL \"%v\"", pe.Name, pe.Job.URL) log.Debugf("Job %v configured with folder \"%v\"", pe.Name, jobFolder) err := os.RemoveAll(jobFolder) if err != nil { return fmt.Errorf("could not delete existing folder %v", jobFolder) } err = os.MkdirAll(jobFolder, 0755) if err != nil { return fmt.Errorf("could not create working directory for job %v: %w", pe.Name, err) } log.Infof("Cloning source from URL %v", pe.Job.URL) // TODO: should I use go-git here instead of shelling out to raw git? cloneCmd := exec.Command("git", "clone", pe.Job.URL, jobFolder) output, err := cloneCmd.CombinedOutput() if err != nil { log.Debugf("%s", output) return fmt.Errorf("could not clone source: %w", err) } cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { return fmt.Errorf("Could not create docker client: %w", err) } log.Info("Source cloned successfully") ctx := context.Background() imageName := "git.ohea.xyz/cursorius/cursorius:latest" log.Infof("Pulling image %v", imageName) pullOutput, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{}) if err != nil { return fmt.Errorf("could not pull image %v: %w", imageName, err) } buf, err := io.ReadAll(pullOutput) if err != nil { return fmt.Errorf("could not read from io.ReadCloser:, %w", err) } log.Infof("%s", buf) err = pullOutput.Close() if err != nil { return fmt.Errorf("could not close io.ReadCloser: %w", err) } log.Info("Image pulled sucessfully") hostConfig := container.HostConfig{} if pipelineConf.DockerNetwork != nil { hostConfig.NetworkMode = container.NetworkMode(*pipelineConf.DockerNetwork) } if pipelineConf.MountConf.Type == config.Bind { hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{ Type: mount.TypeBind, Source: fmt.Sprintf("%v/%v", pipelineConf.MountConf.Source, pe.Name), Target: "/cursorius/src", ReadOnly: false, Consistency: mount.ConsistencyDefault, }, ) } else if pipelineConf.MountConf.Type == config.Volume { hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{ Type: mount.TypeVolume, Source: pipelineConf.MountConf.Source, Target: "/cursorius/src", ReadOnly: false, Consistency: mount.ConsistencyDefault, }, ) } resp, err := cli.ContainerCreate(ctx, &container.Config{ Image: imageName, Cmd: []string{"/launcher.sh"}, Tty: false, Env: []string{ "CURSORIUS_SRC_DIR=/cursorius/src", fmt.Sprintf("CUROSRIUS_SERVER_URL=%v", pipelineConf.AccessURL), }, }, // TODO: fix running the runner in docker (add VolumesFrom to HostConfig) &hostConfig, nil, nil, "", ) if err != nil { return fmt.Errorf("could not create container: %w", err) } log.Info("Launching container") if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { return fmt.Errorf("could not start container: %w", err) } statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil { return fmt.Errorf("container returned error: %w", err) } case retCode := <-statusCh: log.Debugf("Container finished running with return code: %v", retCode) } out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true}) if err != nil { return fmt.Errorf("could not get container logs: %w", err) } var stdOut bytes.Buffer var stdErr bytes.Buffer stdcopy.StdCopy(&stdOut, &stdErr, out) log.Debugf("%s", stdOut.Bytes()) log.Debugf("%s", stdErr.Bytes()) return nil }