diff --git a/Dockerfile b/Dockerfile index 8cb76a0..374bc5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,14 +15,14 @@ WORKDIR /workspace/loom RUN go mod download # Copy the go source -COPY loom/cmd/main.go cmd/main.go +COPY loom/cmd/controller/main.go cmd/controller/main.go COPY loom/api/ api/ COPY loom/internal/ internal/ COPY loom/pkg/ pkg/ # Build # CGO is required for go-sqlite3 -RUN CGO_ENABLED=1 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go +RUN CGO_ENABLED=1 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/controller/main.go # Use distroless base image with glibc (not static) since we need CGO # Refer to https://github.com/GoogleContainerTools/distroless for more details diff --git a/Makefile b/Makefile index c55ed4f..b902f24 100644 --- a/Makefile +++ b/Makefile @@ -157,11 +157,11 @@ lint-config: golangci-lint ## Verify golangci-lint linter configuration .PHONY: build build: manifests generate fmt vet ## Build manager binary. - go build -o bin/manager cmd/main.go + go build -o bin/manager cmd/controller/main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. - go run ./cmd/main.go + go run ./cmd/controller/main.go # If you wish to build the manager image targeting other platforms you can use the --platform flag. # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. @@ -174,6 +174,88 @@ docker-build: ## Build docker image with the manager. docker-push: ## Push docker image with the manager. $(CONTAINER_TOOL) push ${IMG} +# Runner image configuration +RUNNER_IMG ?= atcr.io/evan.jarrett.net/loom-runner:$(VERSION) +RUNNER_IMG_LATEST ?= atcr.io/evan.jarrett.net/loom-runner:latest +RUNNER_PLATFORMS ?= linux/amd64,linux/arm64 + +.PHONY: setup-buildx-runner +setup-buildx-runner: ## Set up buildx builder with credential access for runner builds + @echo "Setting up loom-builder with credential access..." + # Remove existing builder if present + - $(CONTAINER_TOOL) buildx rm loom-builder 2>/dev/null || true + # Create builder with Docker config and credential helper mounts + $(CONTAINER_TOOL) buildx create --name loom-builder \ + --driver docker-container \ + --driver-opt network=host \ + --driver-opt "env.PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ + --config /dev/stdin <<< 'debug = true' || true + # Bootstrap the builder to mount credentials + $(CONTAINER_TOOL) buildx inspect --bootstrap loom-builder + # Copy Docker config and credential helper into the builder container + @echo "Copying credentials to builder container..." + @$(CONTAINER_TOOL) exec buildx_buildkit_loom-builder0 mkdir -p /root/.docker /usr/local/sbin 2>/dev/null || true + $(CONTAINER_TOOL) cp $(HOME)/.docker/config.json buildx_buildkit_loom-builder0:/root/.docker/config.json + @if [ -f /usr/local/sbin/docker-credential-atcr ]; then \ + echo "Copying credential helper to builder container..."; \ + $(CONTAINER_TOOL) cp /usr/local/sbin/docker-credential-atcr buildx_buildkit_loom-builder0:/usr/local/sbin/docker-credential-atcr && \ + $(CONTAINER_TOOL) exec buildx_buildkit_loom-builder0 chmod +x /usr/local/sbin/docker-credential-atcr; \ + echo "Installing glibc compatibility for credential helper (Alpine container)..."; \ + $(CONTAINER_TOOL) exec buildx_buildkit_loom-builder0 apk add --no-cache gcompat libc6-compat 2>/dev/null || \ + echo "⚠ Could not install glibc compatibility, credential helper may not work"; \ + echo "✓ Credential helper installed in builder"; \ + else \ + echo "⚠ Credential helper not found, will rely on username/password auth"; \ + fi + @echo "✓ Builder setup complete!" + +.PHONY: docker-build-runner +docker-build-runner: setup-buildx-runner ## Build multi-arch docker image for the loom runner binary. + @if [ -n "$(DOCKER_USERNAME)" ] && [ -n "$(DOCKER_PASSWORD)" ]; then \ + echo "Authenticating to atcr.io with username/password..."; \ + echo "$(DOCKER_PASSWORD)" | $(CONTAINER_TOOL) login atcr.io -u "$(DOCKER_USERNAME)" --password-stdin; \ + echo "Copying updated credentials to builder..."; \ + $(CONTAINER_TOOL) cp $(HOME)/.docker/config.json buildx_buildkit_loom-builder0:/root/.docker/config.json; \ + else \ + echo "⚠ No DOCKER_USERNAME/DOCKER_PASSWORD set. Will try credential helper (may not work in container)."; \ + fi + cd .. && $(CONTAINER_TOOL) buildx build \ + --builder loom-builder \ + --platform=$(RUNNER_PLATFORMS) \ + --tag $(RUNNER_IMG) \ + --tag $(RUNNER_IMG_LATEST) \ + --push \ + --file loom/cmd/runner/Dockerfile \ + . + +.PHONY: docker-build-runner-local +docker-build-runner-local: ## Build local runner image (single arch) for testing. + cd .. && $(CONTAINER_TOOL) build \ + --tag $(RUNNER_IMG_LATEST) \ + --file loom/cmd/runner/Dockerfile \ + . + +.PHONY: test-registry-auth +test-registry-auth: ## Test registry authentication before building + @echo "Testing registry authentication for atcr.io..." + @if [ -f /usr/local/sbin/docker-credential-atcr ]; then \ + echo "Testing credential helper..."; \ + echo "atcr.io" | docker-credential-atcr get && echo "✓ Credential helper working!" || echo "✗ Credential helper failed"; \ + else \ + echo "⚠ Credential helper not found at /usr/local/sbin/docker-credential-atcr"; \ + fi + @echo "" + @echo "Testing Docker config..." + @if [ -f $(HOME)/.docker/config.json ]; then \ + echo "✓ Docker config exists at $(HOME)/.docker/config.json"; \ + cat $(HOME)/.docker/config.json | grep -q "atcr.io" && echo "✓ atcr.io found in config" || echo "⚠ atcr.io not found in config"; \ + else \ + echo "✗ Docker config not found"; \ + fi + @echo "" + @echo "Testing registry access with docker pull (this will fail if auth is broken)..." + @$(CONTAINER_TOOL) pull atcr.io/evan.jarrett.net/loom-runner:latest 2>/dev/null && echo "✓ Can pull from registry!" || echo "⚠ Cannot pull from registry (may not exist yet)" + # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ diff --git a/cmd/main.go b/cmd/controller/main.go similarity index 85% rename from cmd/main.go rename to cmd/controller/main.go index f1756cc..273f6df 100644 --- a/cmd/main.go +++ b/cmd/controller/main.go @@ -30,6 +30,7 @@ import ( "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -59,7 +60,19 @@ type LoomConfig struct { // LoomTemplateConfig holds job template configuration type LoomTemplateConfig struct { - Resources corev1.ResourceRequirements `yaml:"resources"` + Resources ResourceConfig `yaml:"resources"` +} + +// ResourceConfig holds resource requirements as strings for parsing +type ResourceConfig struct { + Requests ResourceValues `yaml:"requests"` + Limits ResourceValues `yaml:"limits"` +} + +// ResourceValues holds CPU and memory as strings +type ResourceValues struct { + CPU string `yaml:"cpu"` + Memory string `yaml:"memory"` } var ( @@ -91,6 +104,48 @@ func loadLoomConfig(configPath string) (*LoomConfig, error) { return &cfg, nil } +// convertToResourceRequirements converts string-based resource config to Kubernetes ResourceRequirements +func convertToResourceRequirements(cfg ResourceConfig) (corev1.ResourceRequirements, error) { + reqs := corev1.ResourceRequirements{ + Requests: corev1.ResourceList{}, + Limits: corev1.ResourceList{}, + } + + // Parse requests + if cfg.Requests.CPU != "" { + cpuQty, err := resource.ParseQuantity(cfg.Requests.CPU) + if err != nil { + return reqs, fmt.Errorf("invalid CPU request %q: %w", cfg.Requests.CPU, err) + } + reqs.Requests[corev1.ResourceCPU] = cpuQty + } + if cfg.Requests.Memory != "" { + memQty, err := resource.ParseQuantity(cfg.Requests.Memory) + if err != nil { + return reqs, fmt.Errorf("invalid memory request %q: %w", cfg.Requests.Memory, err) + } + reqs.Requests[corev1.ResourceMemory] = memQty + } + + // Parse limits + if cfg.Limits.CPU != "" { + cpuQty, err := resource.ParseQuantity(cfg.Limits.CPU) + if err != nil { + return reqs, fmt.Errorf("invalid CPU limit %q: %w", cfg.Limits.CPU, err) + } + reqs.Limits[corev1.ResourceCPU] = cpuQty + } + if cfg.Limits.Memory != "" { + memQty, err := resource.ParseQuantity(cfg.Limits.Memory) + if err != nil { + return reqs, fmt.Errorf("invalid memory limit %q: %w", cfg.Limits.Memory, err) + } + reqs.Limits[corev1.ResourceMemory] = memQty + } + + return reqs, nil +} + // initializeSpindle creates a spindle server with KubernetesEngine func initializeSpindle(ctx context.Context, cfg *config.Config, mgr ctrl.Manager, loomCfg *LoomConfig) (*spindle.Spindle, error) { // Initialize Kubernetes engine @@ -100,9 +155,15 @@ func initializeSpindle(ctx context.Context, cfg *config.Config, mgr ctrl.Manager namespace = "default" } + // Convert resource config to Kubernetes types + resources, err := convertToResourceRequirements(loomCfg.Template.Resources) + if err != nil { + return nil, fmt.Errorf("failed to convert resource requirements: %w", err) + } + // Create template from loom config template := loomv1alpha1.SpindleTemplate{ - Resources: loomCfg.Template.Resources, + Resources: resources, } kubeEngine := engine.NewKubernetesEngine(mgr.GetClient(), mgr.GetConfig(), namespace, template) @@ -256,8 +317,8 @@ func main() { } setupLog.Info("Loom configuration loaded", "maxConcurrentJobs", loomCfg.MaxConcurrentJobs, - "cpuRequest", loomCfg.Template.Resources.Requests.Cpu().String(), - "memoryRequest", loomCfg.Template.Resources.Requests.Memory().String()) + "cpuRequest", loomCfg.Template.Resources.Requests.CPU, + "memoryRequest", loomCfg.Template.Resources.Requests.Memory) // Load spindle configuration from environment spindleCfg, err := config.Load(ctx) diff --git a/cmd/runner/Dockerfile b/cmd/runner/Dockerfile new file mode 100644 index 0000000..53ea65a --- /dev/null +++ b/cmd/runner/Dockerfile @@ -0,0 +1,35 @@ +FROM --platform=$BUILDPLATFORM golang:1.25 AS builder + +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace + +# Copy core dependency (from replace directive in go.mod) +COPY core/ core/ + +# Copy loom go mod files +COPY loom/go.mod loom/go.sum loom/ +WORKDIR /workspace/loom +RUN go mod download + +# Copy loom source code +COPY loom/api/ api/ +COPY loom/cmd/runner/ cmd/runner/ + +# Build static binary +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ + go build -a -ldflags '-extldflags "-static"' \ + -o loom-runner ./cmd/runner + +# Use minimal base image +FROM alpine:latest + +# Install ca-certificates for HTTPS +RUN apk --no-cache add ca-certificates + +# Copy the binary from builder (built in /workspace/loom/) +COPY --from=builder /workspace/loom/loom-runner /loom-runner + +# Set entrypoint +ENTRYPOINT ["/loom-runner"] diff --git a/cmd/runner/main.go b/cmd/runner/main.go new file mode 100644 index 0000000..09ae98f --- /dev/null +++ b/cmd/runner/main.go @@ -0,0 +1,180 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "time" + + loomv1alpha1 "tangled.org/evan.jarrett.net/loom/api/v1alpha1" +) + +// LogEvent represents a structured log event emitted by the runner. +type LogEvent struct { + Kind string `json:"kind"` // "control" or "data" + Event string `json:"event,omitempty"` // "start", "end" (for control events) + StepID int `json:"step_id"` // 0-based step index + StepName string `json:"step_name,omitempty"` // Step name + Stream string `json:"stream,omitempty"` // "stdout" or "stderr" (for data events) + Content string `json:"content,omitempty"` // Log line content (for data events) + ExitCode *int `json:"exit_code,omitempty"` // Exit code (for control/end events) + Timestamp string `json:"timestamp"` // ISO 8601 timestamp +} + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + os.Exit(1) + } +} + +func run() error { + // Read workflow spec from environment + workflowJSON := os.Getenv("LOOM_WORKFLOW_SPEC") + if workflowJSON == "" { + return fmt.Errorf("LOOM_WORKFLOW_SPEC environment variable not set") + } + + var workflow loomv1alpha1.WorkflowSpec + if err := json.Unmarshal([]byte(workflowJSON), &workflow); err != nil { + return fmt.Errorf("failed to parse workflow spec: %w", err) + } + + // Set up environment variables + if workflow.Environment != nil { + for k, v := range workflow.Environment { + if err := os.Setenv(k, v); err != nil { + return fmt.Errorf("failed to set environment variable %s: %w", k, err) + } + } + } + + // Execute each step + ctx := context.Background() + for i, step := range workflow.Steps { + if err := executeStep(ctx, i, step); err != nil { + return fmt.Errorf("step %d (%s) failed: %w", i, step.Name, err) + } + } + + return nil +} + +func executeStep(ctx context.Context, stepID int, step loomv1alpha1.WorkflowStep) error { + // Emit step start event + emitControlEvent(stepID, step.Name, "start", nil) + + // Set step-specific environment variables + if step.Environment != nil { + for k, v := range step.Environment { + if err := os.Setenv(k, v); err != nil { + return fmt.Errorf("failed to set step environment variable %s: %w", k, err) + } + } + } + + // Create command + cmd := exec.CommandContext(ctx, "bash", "-c", step.Command) + cmd.Dir = "/tangled/workspace" + + // Capture stdout and stderr + stdout, err := cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("failed to create stdout pipe: %w", err) + } + + stderr, err := cmd.StderrPipe() + if err != nil { + return fmt.Errorf("failed to create stderr pipe: %w", err) + } + + // Start the command + if err := cmd.Start(); err != nil { + exitCode := 1 + emitControlEvent(stepID, step.Name, "end", &exitCode) + return fmt.Errorf("failed to start command: %w", err) + } + + // Stream stdout and stderr concurrently + done := make(chan error, 2) + go streamOutput(stdout, stepID, "stdout", done) + go streamOutput(stderr, stepID, "stderr", done) + + // Wait for both streams to complete + for i := 0; i < 2; i++ { + if err := <-done; err != nil { + // Log error but don't fail - we still want to wait for the command + fmt.Fprintf(os.Stderr, "WARNING: error streaming output: %v\n", err) + } + } + + // Wait for command to complete + err = cmd.Wait() + exitCode := 0 + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode = exitErr.ExitCode() + } else { + exitCode = 1 + } + } + + // Emit step end event + emitControlEvent(stepID, step.Name, "end", &exitCode) + + if exitCode != 0 { + return fmt.Errorf("command exited with code %d", exitCode) + } + + return nil +} + +func streamOutput(reader io.Reader, stepID int, stream string, done chan<- error) { + scanner := bufio.NewScanner(reader) + // Increase buffer size for long lines + buf := make([]byte, 0, 64*1024) + scanner.Buffer(buf, 1024*1024) + + for scanner.Scan() { + line := scanner.Text() + emitDataEvent(stepID, stream, line) + } + + done <- scanner.Err() +} + +func emitControlEvent(stepID int, stepName, event string, exitCode *int) { + ev := LogEvent{ + Kind: "control", + Event: event, + StepID: stepID, + StepName: stepName, + ExitCode: exitCode, + Timestamp: time.Now().UTC().Format(time.RFC3339Nano), + } + emitJSON(ev) +} + +func emitDataEvent(stepID int, stream, content string) { + ev := LogEvent{ + Kind: "data", + StepID: stepID, + Stream: stream, + Content: content, + Timestamp: time.Now().UTC().Format(time.RFC3339Nano), + } + emitJSON(ev) +} + +func emitJSON(event LogEvent) { + data, err := json.Marshal(event) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: failed to marshal JSON: %v\n", err) + return + } + fmt.Println(string(data)) +} diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 64ebf27..f769239 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -8,4 +8,4 @@ kind: Kustomization images: - name: controller newName: atcr.io/evan.jarrett.net/loom - newTag: v0.0.4 + newTag: v0.0.6 diff --git a/internal/engine/kubernetes_engine.go b/internal/engine/kubernetes_engine.go index 8870e62..498d940 100644 --- a/internal/engine/kubernetes_engine.go +++ b/internal/engine/kubernetes_engine.go @@ -3,10 +3,9 @@ package engine import ( "bufio" "context" + "encoding/json" "fmt" "io" - "regexp" - "strconv" "strings" "time" @@ -405,54 +404,64 @@ func (e *KubernetesEngine) streamJobLogs(ctx context.Context, job *batchv1.Job, return e.parseLogs(stream, workflow, wfLogger) } -// parseLogs reads log lines, detects step transitions, and sends logs to WorkflowLogger +// LogEvent represents a structured log event from the runner binary +type LogEvent struct { + Kind string `json:"kind"` // "control" or "data" + Event string `json:"event,omitempty"` // "start", "end" (for control events) + StepID int `json:"step_id"` // 0-based step index + StepName string `json:"step_name,omitempty"` // Step name + Stream string `json:"stream,omitempty"` // "stdout" or "stderr" (for data events) + Content string `json:"content,omitempty"` // Log line content (for data events) + ExitCode *int `json:"exit_code,omitempty"` // Exit code (for control/end events) + Timestamp string `json:"timestamp"` // ISO 8601 timestamp +} + +// parseLogs reads log lines as JSON events and sends them to WorkflowLogger func (e *KubernetesEngine) parseLogs(stream io.Reader, workflow *models.Workflow, wfLogger *models.WorkflowLogger) error { scanner := bufio.NewScanner(stream) - - // Regex patterns to detect step transitions - startPattern := regexp.MustCompile(`===== Starting Step (\d+): (.+) =====`) - completePattern := regexp.MustCompile(`===== Completed Step (\d+): (.+) =====`) - - currentStep := -1 // -1 means no step started yet (init containers) - stepStarted := make(map[int]bool) + // Increase buffer size for long lines + buf := make([]byte, 0, 64*1024) + scanner.Buffer(buf, 1024*1024) for scanner.Scan() { line := scanner.Text() - // Check for step start - if matches := startPattern.FindStringSubmatch(line); matches != nil { - stepNum, _ := strconv.Atoi(matches[1]) - stepIdx := stepNum - 1 // Convert 1-based to 0-based - - if stepIdx >= 0 && stepIdx < len(workflow.Steps) { - currentStep = stepIdx - - // Send control message for step start - if !stepStarted[stepIdx] { - step := workflow.Steps[stepIdx] - controlWriter := wfLogger.ControlWriter(stepIdx, step, models.StepStatusStart) - controlWriter.Write([]byte{}) - stepStarted[stepIdx] = true - } - } + // Try to parse as JSON event + var event LogEvent + if err := json.Unmarshal([]byte(line), &event); err != nil { + // Not JSON or parse error - log as debug and skip + continue } - // Check for step completion - if matches := completePattern.FindStringSubmatch(line); matches != nil { - stepNum, _ := strconv.Atoi(matches[1]) - stepIdx := stepNum - 1 + // Validate step index + if event.StepID < 0 || event.StepID >= len(workflow.Steps) { + continue + } - if stepIdx >= 0 && stepIdx < len(workflow.Steps) { - step := workflow.Steps[stepIdx] - controlWriter := wfLogger.ControlWriter(stepIdx, step, models.StepStatusEnd) - controlWriter.Write([]byte{}) + step := workflow.Steps[event.StepID] + + switch event.Kind { + case "control": + // Step start/end events + var status models.StepStatus + if event.Event == "start" { + status = models.StepStatusStart + } else if event.Event == "end" { + status = models.StepStatusEnd + } else { + continue // Unknown control event } - } - // Send data log line - if currentStep >= 0 { - dataWriter := wfLogger.DataWriter(currentStep, "stdout") - dataWriter.Write([]byte(line + "\n")) + controlWriter := wfLogger.ControlWriter(event.StepID, step, status) + controlWriter.Write([]byte{}) + + case "data": + // Log output from step + if event.Stream == "" { + event.Stream = "stdout" // Default to stdout + } + dataWriter := wfLogger.DataWriter(event.StepID, event.Stream) + dataWriter.Write([]byte(event.Content + "\n")) } } diff --git a/pkg/jobbuilder/job_template.go b/pkg/jobbuilder/job_template.go index 589c155..c712bcc 100644 --- a/pkg/jobbuilder/job_template.go +++ b/pkg/jobbuilder/job_template.go @@ -1,6 +1,7 @@ package jobbuilder import ( + "encoding/json" "fmt" "strings" @@ -29,7 +30,10 @@ type WorkflowConfig struct { // Architecture is the target architecture (amd64, arm64) Architecture string - // Steps are the workflow steps to execute + // WorkflowSpec is the full workflow specification to pass to the runner + WorkflowSpec loomv1alpha1.WorkflowSpec + + // Steps are the workflow steps to execute (deprecated - use WorkflowSpec) Steps []WorkflowStep // RepoURL is the git repository URL to clone @@ -66,8 +70,11 @@ func BuildJob(config WorkflowConfig) (*batchv1.Job, error) { return nil, fmt.Errorf("spindleset name is required") } - // Generate the step execution script - script := BuildStepExecutionScript(config.Steps) + // Marshal workflow spec to JSON for the runner binary + workflowSpecJSON, err := json.Marshal(config.WorkflowSpec) + if err != nil { + return nil, fmt.Errorf("failed to marshal workflow spec: %w", err) + } // Build architecture-based node affinity archAffinity := BuildArchitectureAffinity(config.Architecture) @@ -143,8 +150,25 @@ func BuildJob(config WorkflowConfig) (*batchv1.Job, error) { }, }, - // Init container: clone repository + // Init containers: install runner binary, then clone repository InitContainers: []corev1.Container{ + { + Name: "install-runner", + Image: "atcr.io/evan.jarrett.net/loom-runner:latest", + Command: []string{"cp", "/loom-runner", "/runner-bin/loom-runner"}, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &[]bool{false}[0], + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "runner-binary", + MountPath: "/runner-bin", + }, + }, + }, { Name: "clone-repo", Image: "alpine/git:latest", @@ -181,13 +205,12 @@ echo "Repository ready" }, }, - // Main container: execute workflow steps + // Main container: run loom-runner binary in user's image Containers: []corev1.Container{ { Name: "runner", Image: config.Image, - Command: []string{"/bin/bash", "-c"}, - Args: []string{script}, + Command: []string{"/runner-bin/loom-runner"}, WorkingDir: "/tangled/workspace", SecurityContext: &corev1.SecurityContext{ @@ -205,16 +228,15 @@ echo "Repository ready" MountPath: "/tangled/workspace", }, { - Name: "step-outputs", - MountPath: "/tmp/step-outputs", - }, - { - Name: "github-env", - MountPath: "/tmp/github", + Name: "runner-binary", + MountPath: "/runner-bin", }, }, - Env: buildEnvironmentVariables(config), + Env: append(buildEnvironmentVariables(config), corev1.EnvVar{ + Name: "LOOM_WORKFLOW_SPEC", + Value: string(workflowSpecJSON), + }), }, }, @@ -227,13 +249,7 @@ echo "Repository ready" }, }, { - Name: "step-outputs", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }, - { - Name: "github-env", + Name: "runner-binary", VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{}, },