-
Notifications
You must be signed in to change notification settings - Fork 4
feat: wire DEVSY_AGENT_PATH to entrypoint #1114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,17 +15,42 @@ import ( | |
| "k8s.io/apimachinery/pkg/util/wait" | ||
| ) | ||
|
|
||
| const containerRestartAttempts = 3 | ||
| type containerState string | ||
|
|
||
| const ( | ||
| containerStatusRunning containerState = "running" | ||
| containerStatusExited containerState = "exited" | ||
| containerStatusCreated containerState = "created" | ||
| containerStatusPaused containerState = "paused" | ||
| containerStatusRestarting containerState = "restarting" | ||
| containerStatusDead containerState = "dead" | ||
| containerStatusRemoving containerState = "removing" | ||
| ) | ||
|
|
||
| const containerStatusRunning = "running" | ||
| var containerStates = map[string]containerState{ | ||
| "running": containerStatusRunning, | ||
| "exited": containerStatusExited, | ||
| "created": containerStatusCreated, | ||
| "paused": containerStatusPaused, | ||
| "restarting": containerStatusRestarting, | ||
| "dead": containerStatusDead, | ||
| "removing": containerStatusRemoving, | ||
| } | ||
|
|
||
| // snapshotImageLabel marks a committed image as a devsy workspace snapshot, | ||
| // so it's identifiable via `docker inspect`/`docker images --filter` by | ||
| // anyone who pulls or lists it outside `devsy snapshot` tooling — the | ||
| // snapshot manifest (pkg/snapshot) already carries richer sh.devsy.snapshot.* | ||
| // metadata, but that lives in a separate OCI artifact a raw image pull won't | ||
| // see. | ||
| const snapshotImageLabel = "sh.devsy.snapshot=true" | ||
| func toContainerState(s string) containerState { | ||
| if state, ok := containerStates[strings.ToLower(s)]; ok { | ||
| return state | ||
| } | ||
| return containerState(s) | ||
| } | ||
|
|
||
| const ( | ||
| containerRestartAttempts = 3 | ||
|
|
||
| // snapshotImageLabel marks a committed image as a devsy workspace snapshot, | ||
| // so it's identifiable via `docker inspect`/`docker images --filter`. | ||
| snapshotImageLabel = "sh.devsy.snapshot=true" | ||
| ) | ||
|
|
||
| func (d *dockerDriver) CommandDevContainer( | ||
| ctx context.Context, | ||
|
|
@@ -58,75 +83,82 @@ func (d *dockerDriver) CommandDevContainer( | |
| return nil | ||
| } | ||
|
|
||
| // ensureContainerRunning checks that the given container is running, and if | ||
| // not, attempts to start it and wait for it to be running. If the container is | ||
| // in a terminal state (dead or removing), it returns an error. | ||
| // ensureContainerRunning checks the container's state and starts it if necessary. | ||
| func (d *dockerDriver) ensureContainerRunning( | ||
| ctx context.Context, | ||
| container *config.ContainerDetails, | ||
| ) error { | ||
| status := strings.ToLower(container.State.Status) | ||
| if status == "dead" || status == "removing" { | ||
| status := toContainerState(container.State.Status) | ||
| switch status { | ||
| case containerStatusRunning: | ||
| return nil | ||
| case containerStatusDead, containerStatusRemoving: | ||
| return fmt.Errorf( | ||
| "%w: container %s is %q", | ||
| docker.ErrContainerTerminal, | ||
| container.ID, | ||
| status, | ||
| ) | ||
| case containerStatusExited, containerStatusCreated, | ||
| containerStatusPaused, containerStatusRestarting: | ||
| return d.restartAndWait(ctx, container) | ||
|
Comment on lines
+102
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Handle
🤖 Prompt for AI Agents |
||
| default: | ||
| return fmt.Errorf( | ||
| "%w: container %s is in unknown state %q", | ||
| docker.ErrContainerTerminal, | ||
| container.ID, | ||
| status, | ||
| ) | ||
| } | ||
| if status == containerStatusRunning { | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| // restartAndWait starts the container and waits for it to be running, | ||
| // retrying up to containerRestartAttempts times. It aborts immediately when | ||
| // the container enters a terminal state. | ||
| func (d *dockerDriver) restartAndWait( | ||
| ctx context.Context, | ||
| container *config.ContainerDetails, | ||
| ) error { | ||
| var lastErr error | ||
| for attempt := 1; attempt <= containerRestartAttempts; attempt++ { | ||
| if err := ctx.Err(); err != nil { | ||
| return err | ||
| } | ||
| log.Infof( | ||
| "container %s is not running (status=%s), restarting (attempt %d/%d)", | ||
| container.ID, status, attempt, containerRestartAttempts, | ||
| "restarting container %s (status=%s, attempt=%d/%d)", | ||
| container.ID, container.State.Status, attempt, containerRestartAttempts, | ||
| ) | ||
| err := d.restartAndWait(ctx, container.ID) | ||
| if err == nil { | ||
| log.Infof("container %s is now running", container.ID) | ||
| if err := d.Docker.StartContainer(ctx, container.ID); err != nil { | ||
| lastErr = fmt.Errorf("start container: %w", err) | ||
| } else if err := d.Docker.WaitContainerRunning(ctx, container.ID); err != nil { | ||
| lastErr = fmt.Errorf("wait for container to be running: %w", err) | ||
| } else { | ||
| log.Infof("container %s is running", container.ID) | ||
| return nil | ||
| } | ||
| if errors.Is(err, docker.ErrContainerTerminal) { | ||
| return err | ||
| if errors.Is(lastErr, docker.ErrContainerTerminal) || | ||
| errors.Is(lastErr, context.Canceled) || | ||
| errors.Is(lastErr, context.DeadlineExceeded) { | ||
| return lastErr | ||
| } | ||
| lastErr = err | ||
| log.Debugf("container %s restart attempt %d failed: %v", container.ID, attempt, err) | ||
| log.Debugf("container %s restart attempt %d failed: %v", container.ID, attempt, lastErr) | ||
| } | ||
|
|
||
| return fmt.Errorf( | ||
| "%w: container %s did not stay running after %d restart attempts: %v", | ||
| "%w: container %s did not stay running after %d attempts: %w", | ||
| docker.ErrContainerTerminal, container.ID, containerRestartAttempts, lastErr, | ||
| ) | ||
| } | ||
|
|
||
| func (d *dockerDriver) restartAndWait(ctx context.Context, containerID string) error { | ||
| if err := d.Docker.StartContainer(ctx, containerID); err != nil { | ||
| return fmt.Errorf("restart container: %w", err) | ||
| } | ||
| if err := d.Docker.WaitContainerRunning(ctx, containerID); err != nil { | ||
| return fmt.Errorf("wait for container to be running: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (d *dockerDriver) PushDevContainer(ctx context.Context, image string) error { | ||
| // push image | ||
| writer := log.Writer(log.LevelInfo) | ||
| defer func() { _ = writer.Close() }() | ||
|
|
||
| // build args | ||
| args := []string{ | ||
| "push", | ||
| image, | ||
| } | ||
|
|
||
| // run command | ||
| log.Debugf( | ||
| "running docker push command: command=%s, args=%s", | ||
| d.Docker.DockerCommand, | ||
|
|
@@ -141,18 +173,15 @@ func (d *dockerDriver) PushDevContainer(ctx context.Context, image string) error | |
| } | ||
|
|
||
| func (d *dockerDriver) TagDevContainer(ctx context.Context, image, tag string) error { | ||
| // Tag image | ||
| writer := log.Writer(log.LevelInfo) | ||
| defer func() { _ = writer.Close() }() | ||
|
|
||
| // build args | ||
| args := []string{ | ||
| "tag", | ||
| image, | ||
| tag, | ||
| } | ||
|
|
||
| // run command | ||
| log.Debugf( | ||
| "running docker tag command: command=%s, args=%s", | ||
| d.Docker.DockerCommand, | ||
|
|
@@ -201,7 +230,7 @@ func (d *dockerDriver) DeleteDevContainer(ctx context.Context, workspaceId strin | |
| return nil | ||
| } | ||
|
|
||
| if strings.ToLower(container.State.Status) == containerStatusRunning { | ||
| if status := toContainerState(container.State.Status); status == containerStatusRunning { | ||
| if err := d.Docker.Stop(ctx, container.ID); err != nil { | ||
| log.Warnf("stop before delete failed for %s: %v", container.ID, err) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: devsy-org/devsy
Length of output: 50371
🏁 Script executed:
Repository: devsy-org/devsy
Length of output: 41357
🌐 Web query:
k8s.io/apimachinery v0.36.3 PollUntilContextTimeout source context.DeadlineExceeded💡 Result:
In k8s.io/apimachinery v0.36.3, the function PollUntilContextTimeout is the recommended, context-aware method for polling with a duration limit [1][2]. Unlike older, deprecated polling functions (such as Poll and PollImmediate) that returned the specific error wait.ErrWaitTimeout on timeout, PollUntilContextTimeout returns the error defined by the context package [1][3]. When the timeout duration is reached, PollUntilContextTimeout returns context.DeadlineExceeded [3][4]. Key Migration and Usage Notes: 1. Error Handling: Because this function returns errors directly from the context package, you should check for timeouts using errors.Is(err, context.DeadlineExceeded) rather than checking for wait.ErrWaitTimeout [3][5]. 2. Context Cancellation: If the provided context is cancelled for reasons other than the timeout, the function returns context.Canceled [3][4]. 3. Implementation: The function operates by creating a deadline-based context (via context.WithTimeout) and then delegating to PollUntilContextCancel [1]. It does not return the legacy wait.ErrWaitTimeout sentinel [1][4]. 4. Immediate Execution: If you set the immediate parameter to true, the condition function is generally invoked immediately; however, note that historically there have been nuances regarding whether it executes if the context is already cancelled, so ensure your condition function handles the provided context appropriately [6].
Citations:
🏁 Script executed:
Repository: devsy-org/devsy
Length of output: 15248
Preserve
pollErrin the returned error.If inspection fails with a non-context error and polling times out or is canceled, wrapping only
lastErrhidescontext.DeadlineExceededorcontext.CanceledfromrestartAndWait. WrappollErrand retainlastErrin the message. Add regression tests for both paths.🤖 Prompt for AI Agents