···
···
func New(ctx context.Context, db *db.DB, n *notifier.Notifier) (*Engine, error) {
···
l := log.FromContext(ctx).With("component", "spindle")
-
return &Engine{docker: dcli, l: l, db: db, n: n}, nil
// SetupPipeline sets up a new network for the pipeline, and possibly volumes etc.
···
// ONLY marks pipeline as failed if container's exit code is non-zero.
// All other errors are bubbled up.
func (e *Engine) StartSteps(ctx context.Context, steps []*tangled.Pipeline_Step, id, image string) error {
for _, step := range steps {
hostConfig := hostConfig(id)
resp, err := e.docker.ContainerCreate(ctx, &container.Config{
···
-
err := e.TailStep(ctx, resp.ID)
e.l.Error("failed to tail container", "container", resp.ID)
···
-
func (e *Engine) TailStep(ctx context.Context, containerID string) error {
logs, err := e.docker.ContainerLogs(ctx, containerID, container.LogsOptions{
···
-
_, _ = stdcopy.StdCopy(os.Stdout, os.Stdout, logs)
func workspaceVolume(id string) string {
···
···
+
stdoutChans map[string]chan string
+
stderrChans map[string]chan string
func New(ctx context.Context, db *db.DB, n *notifier.Notifier) (*Engine, error) {
···
l := log.FromContext(ctx).With("component", "spindle")
+
e.stdoutChans = make(map[string]chan string, 100)
+
e.stderrChans = make(map[string]chan string, 100)
// SetupPipeline sets up a new network for the pipeline, and possibly volumes etc.
···
// ONLY marks pipeline as failed if container's exit code is non-zero.
// All other errors are bubbled up.
func (e *Engine) StartSteps(ctx context.Context, steps []*tangled.Pipeline_Step, id, image string) error {
+
// set up logging channels
+
if _, exists := e.stdoutChans[id]; !exists {
+
e.stdoutChans[id] = make(chan string, 100)
+
if _, exists := e.stderrChans[id]; !exists {
+
e.stderrChans[id] = make(chan string, 100)
+
// close channels after all steps are complete
+
close(e.stdoutChans[id])
+
close(e.stderrChans[id])
for _, step := range steps {
hostConfig := hostConfig(id)
resp, err := e.docker.ContainerCreate(ctx, &container.Config{
···
+
err := e.TailStep(ctx, resp.ID, id)
e.l.Error("failed to tail container", "container", resp.ID)
···
+
func (e *Engine) TailStep(ctx context.Context, containerID, pipelineID string) error {
logs, err := e.docker.ContainerLogs(ctx, containerID, container.LogsOptions{
···
+
// using StdCopy we demux logs and stream stdout and stderr to different
+
// stdout w||r stdoutCh
+
// stderr w||r stderrCh
+
rpipeOut, wpipeOut := io.Pipe()
+
rpipeErr, wpipeErr := io.Pipe()
+
_, err := stdcopy.StdCopy(wpipeOut, wpipeErr, logs)
+
if err != nil && err != io.EOF {
+
e.l.Error("failed to copy logs", "error", err)
+
// read from stdout and send to stdout pipe
+
// NOTE: the stdoutCh channnel is closed further up in StartSteps
+
// once all steps are done.
+
stdoutCh := e.stdoutChans[pipelineID]
+
scanner := bufio.NewScanner(rpipeOut)
+
stdoutCh <- scanner.Text()
+
if err := scanner.Err(); err != nil {
+
e.l.Error("failed to scan stdout", "error", err)
+
// read from stderr and send to stderr pipe
+
// NOTE: the stderrCh channnel is closed further up in StartSteps
+
// once all steps are done.
+
stderrCh := e.stderrChans[pipelineID]
+
scanner := bufio.NewScanner(rpipeErr)
+
stderrCh <- scanner.Text()
+
if err := scanner.Err(); err != nil {
+
e.l.Error("failed to scan stderr", "error", err)
+
func (e *Engine) LogChannels(pipelineID string) (stdout <-chan string, stderr <-chan string, ok bool) {
+
defer e.chanMu.RUnlock()
+
stdoutCh, ok1 := e.stdoutChans[pipelineID]
+
stderrCh, ok2 := e.stderrChans[pipelineID]
+
return stdoutCh, stderrCh, true
func workspaceVolume(id string) string {