package nixery import ( "bufio" "context" "fmt" "io" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/pkg/stdcopy" "tangled.org/core/spindle/engine" "tangled.org/core/spindle/models" "tangled.org/core/spindle/storage" ) func (e *Engine) baseEnv() EnvVars { envs := EnvVars{} envs.AddEnv("HOME", homeDir) envs.AddEnv("PATH", fmt.Sprintf("%s/.nix-profile/bin:/nix/var/nix/profiles/default/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", homeDir)) return envs } func (e *Engine) execAttached(ctx context.Context, containerID string, opts container.ExecOptions) (string, types.HijackedResponse, error) { execResp, err := e.docker.ContainerExecCreate(ctx, containerID, opts) if err != nil { return "", types.HijackedResponse{}, fmt.Errorf("create exec: %w", err) } attach, err := e.docker.ContainerExecAttach(ctx, execResp.ID, container.ExecAttachOptions{}) if err != nil { return "", types.HijackedResponse{}, fmt.Errorf("attach exec: %w", err) } return execResp.ID, attach, nil } func (e *Engine) containerID(wf *models.Workflow) (string, error) { addl, ok := wf.Data.(addlFields) if !ok || addl.container == "" { return "", fmt.Errorf("nixery workflow has no container") } return addl.container, nil } func (e *Engine) RestoreCache(ctx context.Context, wid models.WorkflowId, wf *models.Workflow, store storage.Storage, caches []engine.ResolvedCache, wfLogger models.WorkflowLogger) error { containerID, err := e.containerID(wf) if err != nil { return err } out := wfLogger.DataWriter(engine.CacheRestoreStepIdx, "stdout") for _, entry := range caches { if err := ctx.Err(); err != nil { return err } if entry.RestoreKey == "" { fmt.Fprintf(out, "cache %q: miss\n", entry.Key) continue } if entry.RestoreName != "" { fmt.Fprintf(out, "cache %q: restoring from %q\n", entry.Key, entry.RestoreName) } rc, err := store.Get(ctx, entry.RestoreKey) if err != nil { fmt.Fprintf(out, "cache %q: fetch failed: %v\n", entry.Key, err) continue } br := bufio.NewReader(rc) execID, attach, err := e.execAttached(ctx, containerID, container.ExecOptions{ Cmd: []string{"bash", "-c", engine.CacheDecompressCmd(br) + " | tar -x -C /"}, Env: e.baseEnv(), AttachStdin: true, AttachStdout: true, AttachStderr: true, }) if err != nil { rc.Close() return fmt.Errorf("restore cache %q: %w", entry.Key, err) } // drain this now or tar can block on stderr before reading stdin copyDone := make(chan error, 1) go func() { _, err := io.Copy(attach.Conn, br) _ = attach.CloseWrite() copyDone <- err }() _, _ = stdcopy.StdCopy(out, out, attach.Reader) copyErr := <-copyDone rc.Close() attach.Close() if copyErr != nil { return fmt.Errorf("restore cache %q: stream archive: %w", entry.Key, copyErr) } inspect, err := e.docker.ContainerExecInspect(ctx, execID) if err != nil { return fmt.Errorf("restore cache %q: %w", entry.Key, err) } if inspect.ExitCode != 0 { fmt.Fprintf(out, "cache %q: extract failed (exit %d)\n", entry.Key, inspect.ExitCode) continue } fmt.Fprintf(out, "cache %q: restored\n", entry.Key) } return nil } func (e *Engine) SaveCache(ctx context.Context, wid models.WorkflowId, wf *models.Workflow, store storage.Storage, caches []engine.ResolvedCache, wfLogger models.WorkflowLogger) error { containerID, err := e.containerID(wf) if err != nil { return err } out := wfLogger.DataWriter(engine.CacheSaveStepIdx, "stdout") for _, entry := range caches { if err := ctx.Err(); err != nil { return err } script := engine.CacheSaveScript(entry.Paths, workspaceDir, entry.CompressionLevel) execID, attach, err := e.execAttached(ctx, containerID, container.ExecOptions{ Cmd: []string{"bash", "-c", script}, Env: e.baseEnv(), AttachStdout: true, AttachStderr: true, }) if err != nil { return fmt.Errorf("save cache %q: %w", entry.Key, err) } up := engine.NewCacheUpload(ctx, store, entry.SaveKey) // StdCopy only returns once the archive is fully written _, copyErr := stdcopy.StdCopy(up.Writer, out, attach.Reader) attach.Close() inspect, inspectErr := e.docker.ContainerExecInspect(ctx, execID) switch { case inspectErr == nil && inspect.ExitCode == engine.CacheExitNoPaths: up.Abort(fmt.Errorf("no cache paths")) fmt.Fprintf(out, "cache %q: nothing to save\n", entry.Key) continue case inspectErr == nil && inspect.ExitCode == engine.CacheExitNoCompressor: up.Abort(fmt.Errorf("zstd not available")) fmt.Fprintf(out, "cache %q: zstd not available in image; skipping\n", entry.Key) continue case copyErr != nil: up.Abort(copyErr) return fmt.Errorf("save cache %q: stream archive: %w", entry.Key, copyErr) case inspectErr != nil: up.Abort(inspectErr) return fmt.Errorf("save cache %q: %w", entry.Key, inspectErr) case inspect.ExitCode != 0: up.Abort(fmt.Errorf("exited %d", inspect.ExitCode)) return fmt.Errorf("save cache %q: tar exited %d", entry.Key, inspect.ExitCode) } if err := up.Finish(); err != nil { return fmt.Errorf("save cache %q: %w", entry.Key, err) } fmt.Fprintf(out, "cache %q: saved\n", entry.Key) } return nil }