Monorepo for Tangled — https://tangled.org

POC buildah engine

evan.jarrett.net 61256d17 1b21764c

verified
+659 -3
+38
.tangled/workflows/docker-build-example.yml
··· 1 + when: 2 + - event: ["push"] 3 + branch: master 4 + 5 + engine: buildah 6 + 7 + build: 8 + # Path to Dockerfile relative to repository root 9 + dockerfile: "./Dockerfile" 10 + 11 + # Build context directory 12 + context: "." 13 + 14 + # Optional: specify a target for multi-stage builds 15 + # target: "production" 16 + 17 + # Optional: build arguments 18 + build_args: 19 + VERSION: "1.0.0" 20 + GO_VERSION: "1.21" 21 + 22 + # Image destinations (will be tagged and pushed) 23 + destination: 24 + - "docker.io/myorg/tangled-app:latest" 25 + - "docker.io/myorg/tangled-app:${GIT_SHA}" 26 + 27 + # Registry credentials from Spindle secrets 28 + registry_credentials: 29 + username_secret: "DOCKER_USERNAME" 30 + password_secret: "DOCKER_PASSWORD" 31 + registry: "docker.io" 32 + 33 + # Optional: additional steps to run after the build 34 + # steps: 35 + # - name: "Verify image" 36 + # command: | 37 + # echo "Image built successfully!" 38 + # buildah images
+38
Dockerfile.example
··· 1 + # Example Dockerfile for building a Go application with the buildah engine 2 + # This is just an example - adjust based on your needs 3 + 4 + # Build stage 5 + FROM golang:1.21 AS builder 6 + 7 + WORKDIR /app 8 + 9 + # Copy go mod files 10 + COPY go.mod go.sum ./ 11 + RUN go mod download 12 + 13 + # Copy source code 14 + COPY . . 15 + 16 + # Build the applications 17 + RUN mkdir -p appview/pages/static && touch appview/pages/static/x 18 + RUN CGO_ENABLED=1 go build -o /app/appview.out ./cmd/appview 19 + RUN CGO_ENABLED=1 go build -o /app/knot.out ./cmd/knot 20 + RUN CGO_ENABLED=1 go build -o /app/spindle.out ./cmd/spindle 21 + 22 + # Runtime stage 23 + FROM debian:bookworm-slim 24 + 25 + # Install runtime dependencies 26 + RUN apt-get update && apt-get install -y \ 27 + ca-certificates \ 28 + && rm -rf /var/lib/apt/lists/* 29 + 30 + WORKDIR /app 31 + 32 + # Copy binaries from builder 33 + COPY --from=builder /app/appview.out /app/appview 34 + COPY --from=builder /app/knot.out /app/knot 35 + COPY --from=builder /app/spindle.out /app/spindle 36 + 37 + # Default command 38 + CMD ["/app/appview"]
+8 -2
spindle/config/config.go
··· 40 40 WorkflowTimeout string `env:"WORKFLOW_TIMEOUT, default=5m"` 41 41 } 42 42 43 + type BuildahPipelines struct { 44 + Image string `env:"IMAGE, default=quay.io/buildah/stable:latest"` 45 + WorkflowTimeout string `env:"WORKFLOW_TIMEOUT, default=15m"` 46 + } 47 + 43 48 type Config struct { 44 - Server Server `env:",prefix=SPINDLE_SERVER_"` 45 - NixeryPipelines NixeryPipelines `env:",prefix=SPINDLE_NIXERY_PIPELINES_"` 49 + Server Server `env:",prefix=SPINDLE_SERVER_"` 50 + NixeryPipelines NixeryPipelines `env:",prefix=SPINDLE_NIXERY_PIPELINES_"` 51 + BuildahPipelines BuildahPipelines `env:",prefix=SPINDLE_BUILDAH_PIPELINES_"` 46 52 } 47 53 48 54 func Load(ctx context.Context) (*Config, error) {
+127
spindle/engines/buildah/build.go
··· 1 + package buildah 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "io" 7 + "path" 8 + "strings" 9 + 10 + "github.com/docker/docker/api/types/container" 11 + "github.com/docker/docker/api/types/image" 12 + "github.com/docker/docker/api/types/mount" 13 + "tangled.org/core/api/tangled" 14 + "tangled.org/core/spindle/models" 15 + "tangled.org/core/workflow" 16 + ) 17 + 18 + // populateBuildContext clones the repository into the build context volume 19 + func (e *Engine) populateBuildContext(ctx context.Context, wid models.WorkflowId, volumeName string, twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata) error { 20 + // Build git clone commands 21 + var commands []string 22 + 23 + // Initialize git repo in workspace 24 + commands = append(commands, "git init") 25 + 26 + // Add repo as git remote 27 + scheme := "https://" 28 + if e.cfg.Server.Dev { 29 + scheme = "http://" 30 + tr.Repo.Knot = strings.ReplaceAll(tr.Repo.Knot, "localhost", "host.docker.internal") 31 + } 32 + url := scheme + path.Join(tr.Repo.Knot, tr.Repo.Did, tr.Repo.Repo) 33 + commands = append(commands, fmt.Sprintf("git remote add origin %s", url)) 34 + 35 + // Run git fetch 36 + var fetchArgs []string 37 + 38 + // Default clone depth is 1 39 + depth := 1 40 + if twf.Clone.Depth > 1 { 41 + depth = int(twf.Clone.Depth) 42 + } 43 + fetchArgs = append(fetchArgs, fmt.Sprintf("--depth=%d", depth)) 44 + 45 + // Optionally recurse submodules 46 + if twf.Clone.Submodules { 47 + fetchArgs = append(fetchArgs, "--recurse-submodules=yes") 48 + } 49 + 50 + // Set remote to fetch from 51 + fetchArgs = append(fetchArgs, "origin") 52 + 53 + // Set revision to checkout based on trigger type 54 + switch workflow.TriggerKind(tr.Kind) { 55 + case workflow.TriggerKindManual: 56 + // TODO: unimplemented 57 + case workflow.TriggerKindPush: 58 + fetchArgs = append(fetchArgs, tr.Push.NewSha) 59 + case workflow.TriggerKindPullRequest: 60 + fetchArgs = append(fetchArgs, tr.PullRequest.SourceSha) 61 + } 62 + 63 + commands = append(commands, fmt.Sprintf("git fetch %s", strings.Join(fetchArgs, " "))) 64 + 65 + // Run git checkout 66 + commands = append(commands, "git checkout FETCH_HEAD") 67 + 68 + script := strings.Join(commands, "\n") 69 + 70 + // Use alpine/git image to clone the repository 71 + gitImage := "alpine/git:latest" 72 + 73 + // Pull git image 74 + reader, err := e.docker.ImagePull(ctx, gitImage, image.PullOptions{}) 75 + if err != nil { 76 + return fmt.Errorf("pulling git image: %w", err) 77 + } 78 + defer reader.Close() 79 + 80 + // Consume the pull output 81 + io.Copy(io.Discard, reader) 82 + 83 + // Create container to clone repo 84 + resp, err := e.docker.ContainerCreate(ctx, &container.Config{ 85 + Image: gitImage, 86 + Cmd: []string{"sh", "-c", script}, 87 + WorkingDir: workspaceDir, 88 + }, &container.HostConfig{ 89 + Mounts: []mount.Mount{ 90 + { 91 + Type: mount.TypeVolume, 92 + Source: volumeName, 93 + Target: workspaceDir, 94 + }, 95 + }, 96 + }, nil, nil, "") 97 + if err != nil { 98 + return fmt.Errorf("creating git clone container: %w", err) 99 + } 100 + 101 + // Ensure cleanup 102 + defer func() { 103 + e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true}) 104 + }() 105 + 106 + // Start container 107 + err = e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{}) 108 + if err != nil { 109 + return fmt.Errorf("starting git clone container: %w", err) 110 + } 111 + 112 + // Wait for completion 113 + statusCh, errCh := e.docker.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) 114 + select { 115 + case err := <-errCh: 116 + return fmt.Errorf("git clone error: %w", err) 117 + case status := <-statusCh: 118 + if status.StatusCode != 0 { 119 + e.l.Error("git clone failed", "workflow_id", wid, "exit_code", status.StatusCode) 120 + return fmt.Errorf("git clone failed with exit code %d", status.StatusCode) 121 + } 122 + case <-ctx.Done(): 123 + return ctx.Err() 124 + } 125 + 126 + return nil 127 + }
+430
spindle/engines/buildah/engine.go
··· 1 + package buildah 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "io" 7 + "log/slog" 8 + "os" 9 + "path" 10 + "strings" 11 + "sync" 12 + "time" 13 + 14 + "github.com/docker/docker/api/types/container" 15 + "github.com/docker/docker/api/types/image" 16 + "github.com/docker/docker/api/types/mount" 17 + "github.com/docker/docker/api/types/volume" 18 + "github.com/docker/docker/client" 19 + "github.com/docker/docker/pkg/stdcopy" 20 + "gopkg.in/yaml.v3" 21 + "tangled.org/core/api/tangled" 22 + "tangled.org/core/log" 23 + "tangled.org/core/spindle/config" 24 + "tangled.org/core/spindle/engine" 25 + "tangled.org/core/spindle/models" 26 + "tangled.org/core/spindle/secrets" 27 + ) 28 + 29 + const ( 30 + workspaceDir = "/workspace" 31 + homeDir = "/root" 32 + ) 33 + 34 + type cleanupFunc func(context.Context) error 35 + 36 + type Engine struct { 37 + docker client.APIClient 38 + l *slog.Logger 39 + cfg *config.Config 40 + 41 + cleanupMu sync.Mutex 42 + cleanup map[string][]cleanupFunc 43 + } 44 + 45 + type BuildConfig struct { 46 + Dockerfile string `yaml:"dockerfile"` 47 + Context string `yaml:"context"` 48 + Target string `yaml:"target"` 49 + BuildArgs map[string]string `yaml:"build_args"` 50 + Destination []string `yaml:"destination"` 51 + RegistryCredentials struct { 52 + UsernameSecret string `yaml:"username_secret"` 53 + PasswordSecret string `yaml:"password_secret"` 54 + Registry string `yaml:"registry"` 55 + } `yaml:"registry_credentials"` 56 + } 57 + 58 + type Step struct { 59 + name string 60 + kind models.StepKind 61 + command string 62 + } 63 + 64 + func (s Step) Name() string { 65 + return s.name 66 + } 67 + 68 + func (s Step) Command() string { 69 + return s.command 70 + } 71 + 72 + func (s Step) Kind() models.StepKind { 73 + return s.kind 74 + } 75 + 76 + type addlFields struct { 77 + volumeName string 78 + buildCfg BuildConfig 79 + workflow tangled.Pipeline_Workflow 80 + triggerMetadata tangled.Pipeline_TriggerMetadata 81 + } 82 + 83 + func New(ctx context.Context, cfg *config.Config) (*Engine, error) { 84 + dcli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 85 + if err != nil { 86 + return nil, err 87 + } 88 + 89 + l := log.FromContext(ctx).With("component", "buildah-engine") 90 + 91 + e := &Engine{ 92 + docker: dcli, 93 + l: l, 94 + cfg: cfg, 95 + } 96 + 97 + e.cleanup = make(map[string][]cleanupFunc) 98 + 99 + return e, nil 100 + } 101 + 102 + func (e *Engine) InitWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline) (*models.Workflow, error) { 103 + swf := &models.Workflow{} 104 + addl := addlFields{} 105 + 106 + dwf := &struct { 107 + Build BuildConfig `yaml:"build"` 108 + Steps []struct { 109 + Command string `yaml:"command"` 110 + Name string `yaml:"name"` 111 + } `yaml:"steps"` 112 + }{} 113 + 114 + err := yaml.Unmarshal([]byte(twf.Raw), &dwf) 115 + if err != nil { 116 + return nil, err 117 + } 118 + 119 + // Validate build configuration 120 + if dwf.Build.Dockerfile == "" { 121 + dwf.Build.Dockerfile = "Dockerfile" 122 + } 123 + if dwf.Build.Context == "" { 124 + dwf.Build.Context = "." 125 + } 126 + if len(dwf.Build.Destination) == 0 { 127 + return nil, fmt.Errorf("%w: at least one destination required", ErrInvalidConfig) 128 + } 129 + 130 + // Convert user steps 131 + for _, dstep := range dwf.Steps { 132 + sstep := Step{} 133 + sstep.command = dstep.Command 134 + sstep.name = dstep.Name 135 + sstep.kind = models.StepKindUser 136 + swf.Steps = append(swf.Steps, sstep) 137 + } 138 + 139 + swf.Name = twf.Name 140 + addl.buildCfg = dwf.Build 141 + addl.workflow = twf 142 + if tpl.TriggerMetadata != nil { 143 + addl.triggerMetadata = *tpl.TriggerMetadata 144 + } 145 + swf.Data = addl 146 + 147 + return swf, nil 148 + } 149 + 150 + func (e *Engine) WorkflowTimeout() time.Duration { 151 + workflowTimeoutStr := e.cfg.BuildahPipelines.WorkflowTimeout 152 + workflowTimeout, err := time.ParseDuration(workflowTimeoutStr) 153 + if err != nil { 154 + e.l.Error("failed to parse workflow timeout", "error", err, "timeout", workflowTimeoutStr) 155 + workflowTimeout = 15 * time.Minute 156 + } 157 + 158 + return workflowTimeout 159 + } 160 + 161 + func (e *Engine) SetupWorkflow(ctx context.Context, wid models.WorkflowId, wf *models.Workflow) error { 162 + e.l.Info("setting up buildah workflow", "workflow", wid) 163 + 164 + addl := wf.Data.(addlFields) 165 + 166 + // Create Docker volume for build context 167 + volumeName := fmt.Sprintf("buildah-context-%s", wid.String()) 168 + _, err := e.docker.VolumeCreate(ctx, volumeCreateOptions(volumeName)) 169 + if err != nil { 170 + return fmt.Errorf("creating build context volume: %w", err) 171 + } 172 + e.registerCleanup(wid, func(ctx context.Context) error { 173 + return e.docker.VolumeRemove(ctx, volumeName, true) 174 + }) 175 + 176 + // Clone repository into volume using git container 177 + if !addl.workflow.Clone.Skip { 178 + err = e.populateBuildContext(ctx, wid, volumeName, addl.workflow, addl.triggerMetadata) 179 + if err != nil { 180 + return fmt.Errorf("populating build context: %w", err) 181 + } 182 + } 183 + 184 + // Update workflow data with volume name 185 + addl.volumeName = volumeName 186 + wf.Data = addl 187 + 188 + return nil 189 + } 190 + 191 + func (e *Engine) RunStep(ctx context.Context, wid models.WorkflowId, w *models.Workflow, idx int, secrets []secrets.UnlockedSecret, wfLogger *models.WorkflowLogger) error { 192 + addl := w.Data.(addlFields) 193 + 194 + // Check for timeout 195 + select { 196 + case <-ctx.Done(): 197 + return ctx.Err() 198 + default: 199 + } 200 + 201 + // The main build step runs buildah 202 + if idx == 0 { 203 + return e.runBuildahBuild(ctx, wid, addl, secrets, wfLogger) 204 + } 205 + 206 + // User-defined steps run as regular bash commands in a buildah container 207 + step := w.Steps[idx].(Step) 208 + return e.runBashStep(ctx, wid, addl, step, secrets, wfLogger) 209 + } 210 + 211 + func (e *Engine) runBuildahBuild(ctx context.Context, wid models.WorkflowId, addl addlFields, secrets []secrets.UnlockedSecret, wfLogger *models.WorkflowLogger) error { 212 + buildCfg := addl.buildCfg 213 + 214 + // Extract registry credentials from secrets 215 + var registryUser, registryPass string 216 + for _, s := range secrets { 217 + if s.Key == buildCfg.RegistryCredentials.UsernameSecret { 218 + registryUser = s.Value 219 + } 220 + if s.Key == buildCfg.RegistryCredentials.PasswordSecret { 221 + registryPass = s.Value 222 + } 223 + } 224 + 225 + // Build the command script 226 + var scriptParts []string 227 + 228 + // Login to registry if credentials provided 229 + if registryUser != "" && registryPass != "" && buildCfg.RegistryCredentials.Registry != "" { 230 + scriptParts = append(scriptParts, fmt.Sprintf( 231 + `echo "$REGISTRY_PASSWORD" | buildah login -u "$REGISTRY_USERNAME" --password-stdin %s`, 232 + buildCfg.RegistryCredentials.Registry, 233 + )) 234 + } 235 + 236 + // Build command 237 + buildCmd := []string{"buildah", "bud"} 238 + buildCmd = append(buildCmd, "--file", path.Join(workspaceDir, buildCfg.Dockerfile)) 239 + 240 + if buildCfg.Target != "" { 241 + buildCmd = append(buildCmd, "--target", buildCfg.Target) 242 + } 243 + 244 + for k, v := range buildCfg.BuildArgs { 245 + buildCmd = append(buildCmd, "--build-arg", fmt.Sprintf("%s=%s", k, v)) 246 + } 247 + 248 + // Tag with all destinations 249 + for _, dest := range buildCfg.Destination { 250 + buildCmd = append(buildCmd, "--tag", dest) 251 + } 252 + 253 + // Use vfs storage driver (doesn't require privileges) 254 + buildCmd = append(buildCmd, "--storage-driver", "vfs") 255 + buildCmd = append(buildCmd, path.Join(workspaceDir, buildCfg.Context)) 256 + 257 + scriptParts = append(scriptParts, strings.Join(buildCmd, " ")) 258 + 259 + // Push to all destinations 260 + for _, dest := range buildCfg.Destination { 261 + scriptParts = append(scriptParts, fmt.Sprintf("buildah push %s", dest)) 262 + } 263 + 264 + script := strings.Join(scriptParts, "\n") 265 + 266 + // Pull buildah image 267 + reader, err := e.docker.ImagePull(ctx, e.cfg.BuildahPipelines.Image, image.PullOptions{}) 268 + if err != nil { 269 + return fmt.Errorf("pulling buildah image: %w", err) 270 + } 271 + io.Copy(os.Stdout, reader) 272 + reader.Close() 273 + 274 + // Create buildah container 275 + resp, err := e.docker.ContainerCreate(ctx, &container.Config{ 276 + Image: e.cfg.BuildahPipelines.Image, 277 + Cmd: []string{"sh", "-c", script}, 278 + Env: []string{ 279 + fmt.Sprintf("REGISTRY_USERNAME=%s", registryUser), 280 + fmt.Sprintf("REGISTRY_PASSWORD=%s", registryPass), 281 + }, 282 + WorkingDir: workspaceDir, 283 + Labels: map[string]string{ 284 + "sh.tangled.pipeline/workflow_id": wid.String(), 285 + }, 286 + }, &container.HostConfig{ 287 + Mounts: []mount.Mount{ 288 + { 289 + Type: mount.TypeVolume, 290 + Source: addl.volumeName, 291 + Target: workspaceDir, 292 + }, 293 + }, 294 + CapAdd: []string{"CAP_SETUID", "CAP_SETGID"}, 295 + SecurityOpt: []string{"no-new-privileges"}, 296 + }, nil, nil, "") 297 + if err != nil { 298 + return fmt.Errorf("creating buildah container: %w", err) 299 + } 300 + 301 + // Ensure cleanup 302 + defer func() { 303 + e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true}) 304 + }() 305 + 306 + // Start container 307 + err = e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{}) 308 + if err != nil { 309 + return fmt.Errorf("starting buildah container: %w", err) 310 + } 311 + 312 + // Stream logs 313 + if wfLogger != nil { 314 + go e.streamLogs(ctx, resp.ID, wfLogger) 315 + } 316 + 317 + // Wait for completion 318 + statusCh, errCh := e.docker.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) 319 + select { 320 + case err := <-errCh: 321 + return err 322 + case status := <-statusCh: 323 + if status.StatusCode != 0 { 324 + e.l.Error("buildah build failed", "workflow_id", wid, "exit_code", status.StatusCode) 325 + return ErrBuildFailed 326 + } 327 + case <-ctx.Done(): 328 + return engine.ErrTimedOut 329 + } 330 + 331 + return nil 332 + } 333 + 334 + func (e *Engine) runBashStep(ctx context.Context, wid models.WorkflowId, addl addlFields, step Step, secrets []secrets.UnlockedSecret, wfLogger *models.WorkflowLogger) error { 335 + // Similar to runBuildahBuild but just runs the user's command 336 + resp, err := e.docker.ContainerCreate(ctx, &container.Config{ 337 + Image: e.cfg.BuildahPipelines.Image, 338 + Cmd: []string{"sh", "-c", step.Command()}, 339 + WorkingDir: workspaceDir, 340 + }, &container.HostConfig{ 341 + Mounts: []mount.Mount{ 342 + { 343 + Type: mount.TypeVolume, 344 + Source: addl.volumeName, 345 + Target: workspaceDir, 346 + }, 347 + }, 348 + CapAdd: []string{"CAP_SETUID", "CAP_SETGID"}, 349 + SecurityOpt: []string{"no-new-privileges"}, 350 + }, nil, nil, "") 351 + if err != nil { 352 + return fmt.Errorf("creating container for step: %w", err) 353 + } 354 + 355 + defer func() { 356 + e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true}) 357 + }() 358 + 359 + err = e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{}) 360 + if err != nil { 361 + return fmt.Errorf("starting container: %w", err) 362 + } 363 + 364 + if wfLogger != nil { 365 + go e.streamLogs(ctx, resp.ID, wfLogger) 366 + } 367 + 368 + statusCh, errCh := e.docker.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) 369 + select { 370 + case err := <-errCh: 371 + return err 372 + case status := <-statusCh: 373 + if status.StatusCode != 0 { 374 + return engine.ErrWorkflowFailed 375 + } 376 + case <-ctx.Done(): 377 + return engine.ErrTimedOut 378 + } 379 + 380 + return nil 381 + } 382 + 383 + func (e *Engine) streamLogs(ctx context.Context, containerID string, wfLogger *models.WorkflowLogger) error { 384 + logs, err := e.docker.ContainerLogs(ctx, containerID, container.LogsOptions{ 385 + ShowStdout: true, 386 + ShowStderr: true, 387 + Follow: true, 388 + }) 389 + if err != nil { 390 + return err 391 + } 392 + defer logs.Close() 393 + 394 + _, err = stdcopy.StdCopy( 395 + wfLogger.DataWriter("stdout"), 396 + wfLogger.DataWriter("stderr"), 397 + logs, 398 + ) 399 + return err 400 + } 401 + 402 + func (e *Engine) DestroyWorkflow(ctx context.Context, wid models.WorkflowId) error { 403 + e.cleanupMu.Lock() 404 + key := wid.String() 405 + 406 + fns := e.cleanup[key] 407 + delete(e.cleanup, key) 408 + e.cleanupMu.Unlock() 409 + 410 + for _, fn := range fns { 411 + if err := fn(ctx); err != nil { 412 + e.l.Error("failed to cleanup workflow resource", "workflowId", wid, "error", err) 413 + } 414 + } 415 + return nil 416 + } 417 + 418 + func (e *Engine) registerCleanup(wid models.WorkflowId, fn cleanupFunc) { 419 + e.cleanupMu.Lock() 420 + defer e.cleanupMu.Unlock() 421 + 422 + key := wid.String() 423 + e.cleanup[key] = append(e.cleanup[key], fn) 424 + } 425 + 426 + func volumeCreateOptions(name string) volume.CreateOptions { 427 + return volume.CreateOptions{ 428 + Name: name, 429 + } 430 + }
+11
spindle/engines/buildah/errors.go
··· 1 + package buildah 2 + 3 + import "errors" 4 + 5 + var ( 6 + ErrBuildFailed = errors.New("build failed") 7 + ErrPushFailed = errors.New("push failed") 8 + ErrLoginFailed = errors.New("registry login failed") 9 + ErrInvalidConfig = errors.New("invalid build configuration") 10 + ErrMissingDockerfile = errors.New("dockerfile not found") 11 + )
+7 -1
spindle/server.go
··· 20 20 "tangled.org/core/spindle/config" 21 21 "tangled.org/core/spindle/db" 22 22 "tangled.org/core/spindle/engine" 23 + "tangled.org/core/spindle/engines/buildah" 23 24 "tangled.org/core/spindle/engines/nixery" 24 25 "tangled.org/core/spindle/models" 25 26 "tangled.org/core/spindle/queue" ··· 100 101 return err 101 102 } 102 103 104 + buildahEng, err := buildah.New(ctx, cfg) 105 + if err != nil { 106 + return err 107 + } 108 + 103 109 jq := queue.NewQueue(cfg.Server.QueueSize, cfg.Server.MaxJobCount) 104 110 logger.Info("initialized queue", "queueSize", cfg.Server.QueueSize, "numWorkers", cfg.Server.MaxJobCount) 105 111 ··· 131 137 db: d, 132 138 l: logger, 133 139 n: &n, 134 - engs: map[string]models.Engine{"nixery": nixeryEng}, 140 + engs: map[string]models.Engine{"nixery": nixeryEng, "buildah": buildahEng}, 135 141 jq: jq, 136 142 cfg: cfg, 137 143 res: resolver,