package daemon import ( "context" "fmt" "strings" "sync" "time" "banger/internal/api" "banger/internal/model" ) func (op *imageBuildOperationState) ID() string { return op.snapshot().ID } func (op *imageBuildOperationState) IsDone() bool { return op.snapshot().Done } func (op *imageBuildOperationState) UpdatedAt() time.Time { return op.snapshot().UpdatedAt } func (op *imageBuildOperationState) Cancel() { op.cancelOperation() } type imageBuildProgressKey struct{} type imageBuildOperationState struct { mu sync.Mutex cancel context.CancelFunc op api.ImageBuildOperation } func newImageBuildOperationState() (*imageBuildOperationState, error) { id, err := model.NewID() if err != nil { return nil, err } now := model.Now() return &imageBuildOperationState{ op: api.ImageBuildOperation{ ID: id, Stage: "queued", Detail: "waiting to start", StartedAt: now, UpdatedAt: now, }, }, nil } func withImageBuildProgress(ctx context.Context, op *imageBuildOperationState) context.Context { if op == nil { return ctx } return context.WithValue(ctx, imageBuildProgressKey{}, op) } func imageBuildProgressFromContext(ctx context.Context) *imageBuildOperationState { if ctx == nil { return nil } op, _ := ctx.Value(imageBuildProgressKey{}).(*imageBuildOperationState) return op } func imageBuildStage(ctx context.Context, stage, detail string) { if op := imageBuildProgressFromContext(ctx); op != nil { op.stage(stage, detail) } } func imageBuildBindImage(ctx context.Context, image model.Image) { if op := imageBuildProgressFromContext(ctx); op != nil { op.bindImage(image) } } func imageBuildSetLogPath(ctx context.Context, path string) { if op := imageBuildProgressFromContext(ctx); op != nil { op.setLogPath(path) } } func (op *imageBuildOperationState) setCancel(cancel context.CancelFunc) { op.mu.Lock() defer op.mu.Unlock() op.cancel = cancel } func (op *imageBuildOperationState) setLogPath(path string) { op.mu.Lock() defer op.mu.Unlock() op.op.BuildLogPath = strings.TrimSpace(path) op.op.UpdatedAt = model.Now() } func (op *imageBuildOperationState) bindImage(image model.Image) { op.mu.Lock() defer op.mu.Unlock() op.op.ImageID = image.ID op.op.ImageName = image.Name } func (op *imageBuildOperationState) stage(stage, detail string) { op.mu.Lock() defer op.mu.Unlock() stage = strings.TrimSpace(stage) detail = strings.TrimSpace(detail) if stage == "" { stage = op.op.Stage } if stage == op.op.Stage && detail == op.op.Detail { return } op.op.Stage = stage op.op.Detail = detail op.op.UpdatedAt = model.Now() } func (op *imageBuildOperationState) done(image model.Image) { op.mu.Lock() defer op.mu.Unlock() imageCopy := image op.op.ImageID = image.ID op.op.ImageName = image.Name op.op.Stage = "ready" op.op.Detail = "image is ready" op.op.Done = true op.op.Success = true op.op.Error = "" op.op.Image = &imageCopy op.op.UpdatedAt = model.Now() } func (op *imageBuildOperationState) fail(err error) { op.mu.Lock() defer op.mu.Unlock() op.op.Done = true op.op.Success = false if err != nil { op.op.Error = err.Error() } if strings.TrimSpace(op.op.Detail) == "" { op.op.Detail = "image build failed" } op.op.UpdatedAt = model.Now() } func (op *imageBuildOperationState) snapshot() api.ImageBuildOperation { op.mu.Lock() defer op.mu.Unlock() snapshot := op.op if snapshot.Image != nil { imageCopy := *snapshot.Image snapshot.Image = &imageCopy } return snapshot } func (op *imageBuildOperationState) cancelOperation() { op.mu.Lock() cancel := op.cancel op.mu.Unlock() if cancel != nil { cancel() } } func (d *Daemon) BeginImageBuild(_ context.Context, params api.ImageBuildParams) (api.ImageBuildOperation, error) { op, err := newImageBuildOperationState() if err != nil { return api.ImageBuildOperation{}, err } buildCtx, cancel := context.WithCancel(context.Background()) op.setCancel(cancel) d.imageBuildOps.Insert(op) go d.runImageBuildOperation(withImageBuildProgress(buildCtx, op), op, params) return op.snapshot(), nil } func (d *Daemon) runImageBuildOperation(ctx context.Context, op *imageBuildOperationState, params api.ImageBuildParams) { image, err := d.BuildImage(ctx, params) if err != nil { op.fail(err) return } op.done(image) } func (d *Daemon) ImageBuildStatus(_ context.Context, id string) (api.ImageBuildOperation, error) { op, ok := d.imageBuildOps.Get(strings.TrimSpace(id)) if !ok { return api.ImageBuildOperation{}, fmt.Errorf("image build operation not found: %s", id) } return op.snapshot(), nil } func (d *Daemon) CancelImageBuild(_ context.Context, id string) error { op, ok := d.imageBuildOps.Get(strings.TrimSpace(id)) if !ok { return fmt.Errorf("image build operation not found: %s", id) } op.cancelOperation() return nil } func (d *Daemon) pruneImageBuildOperations(olderThan time.Time) { d.imageBuildOps.Prune(olderThan) }