Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 16 additions & 42 deletions cmd/internal/agentcontainer/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,17 @@ func (cmd *SetupContainerCmd) Run(ctx context.Context) error {
tunnelClient: tunnelClient,
}

if err := cmd.prepareWorkspace(sctx); err != nil {
return err
}

return cmd.finalizeSetup(sctx)
_, err = tunnelserver.ReportResult(
ctx,
tunnelClient,
func(_ context.Context) (*config.Result, error) {
if err := cmd.prepareWorkspace(sctx); err != nil {
return nil, err
}
return cmd.finalizeSetup(sctx)
},
)
return err
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

func (cmd *SetupContainerCmd) registerFlags(setupContainerCmd *cobra.Command) {
Expand Down Expand Up @@ -258,10 +264,10 @@ func fetchSecrets(
return env, mount, nil
}

func (cmd *SetupContainerCmd) finalizeSetup(sctx *setupContext) error {
func (cmd *SetupContainerCmd) finalizeSetup(sctx *setupContext) (*config.Result, error) {
secretsEnv, secretsMount, err := fetchSecrets(sctx.ctx, sctx.tunnelClient)
if err != nil {
return cmd.reportSetupFailure(sctx, err)
return nil, err
}
sctx.secretsEnv = secretsEnv

Expand All @@ -287,31 +293,16 @@ func (cmd *SetupContainerCmd) finalizeSetup(sctx *setupContext) error {

deferred, err := setup.SetupContainerPreAttach(sctx.ctx, cfg)
if err != nil {
return cmd.reportSetupFailure(sctx, err)
return nil, err
}

if !cmd.Prebuild {
if err := cmd.setupPostAttach(sctx, deferred); err != nil {
return cmd.reportSetupFailure(sctx, err)
return nil, err
}
}

return cmd.sendSetupResult(sctx.ctx, sctx.setupInfo, sctx.tunnelClient)
}

// reportSetupFailure forwards a structured error result through the tunnel
// before returning the original error. Without this, the outer agent only
// sees the SSH exit code and the underlying cause (e.g. an IDE install
// failure) gets lost to a generic wrapper on the host side.
func (cmd *SetupContainerCmd) reportSetupFailure(sctx *setupContext, cause error) error {
errResult := &config.Result{Error: cause.Error()}
if sendErr := cmd.sendSetupResult(sctx.ctx, errResult, sctx.tunnelClient); sendErr != nil {
// Failure-on-failure: the host will see only the SSH exit code, so
// log the original cause alongside the send failure to leave a
// breadcrumb for debugging.
log.Errorf("failed to forward setup error %q to host: %v", cause, sendErr)
}
return cause
return sctx.setupInfo, nil
}

func (cmd *SetupContainerCmd) setupPostAttach(
Expand Down Expand Up @@ -645,23 +636,6 @@ func (cmd *SetupContainerCmd) startPostAttachHooks(sctx *setupContext) error {
})
}

func (cmd *SetupContainerCmd) sendSetupResult(
ctx context.Context,
setupInfo *config.Result,
tunnelClient tunnel.TunnelClient,
) error {
out, err := json.Marshal(setupInfo)
if err != nil {
return fmt.Errorf("marshal setup info: %w", err)
}

if _, err := tunnelClient.SendResult(ctx, &tunnel.Message{Message: string(out)}); err != nil {
return fmt.Errorf("send result: %w", err)
}

return nil
}

func fillContainerEnv(setupInfo *config.Result) error {
// set remote-env
if setupInfo.MergedConfig.RemoteEnv == nil {
Expand Down
13 changes: 11 additions & 2 deletions cmd/internal/agentworkspace/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (

"github.com/devsy-org/devsy/cmd/flags"
"github.com/devsy-org/devsy/pkg/agent"
"github.com/devsy-org/devsy/pkg/agent/tunnelserver"
"github.com/devsy-org/devsy/pkg/devcontainer"
config2 "github.com/devsy-org/devsy/pkg/devcontainer/config"
cliflags "github.com/devsy-org/devsy/pkg/flags"
"github.com/devsy-org/devsy/pkg/flags/names"
"github.com/devsy-org/devsy/pkg/log"
Expand Down Expand Up @@ -65,7 +67,7 @@ func (cmd *BuildCmd) Run(ctx context.Context) error {
// initialize the workspace
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
_, credentialsDir, err := initWorkspace(cancelCtx, initWorkspaceParams{
tunnelClient, credentialsDir, err := initWorkspace(cancelCtx, initWorkspaceParams{
workspaceInfo: workspaceInfo,
debug: cmd.Debug,
shouldInstallDaemon: false,
Expand All @@ -83,7 +85,14 @@ func (cmd *BuildCmd) Run(ctx context.Context) error {
return err
}

return buildAndPushImages(ctx, runner, workspaceInfo)
_, err = tunnelserver.ReportResult(
cancelCtx,
tunnelClient,
func(ctx context.Context) (*config2.Result, error) {
return nil, buildAndPushImages(ctx, runner, workspaceInfo)
},
)
return err
}

func buildAndPushImages(
Expand Down
51 changes: 17 additions & 34 deletions cmd/internal/agentworkspace/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,53 +150,36 @@ func (cmd *UpCmd) up(
workspaceInfo *provider.AgentWorkspaceInfo,
tunnelClient tunnel.TunnelClient,
) error {
result, err := cmd.devsyUp(ctx, workspaceInfo, tunnelClient)
result, err := tunnelserver.ReportResult(
ctx,
tunnelClient,
func(ctx context.Context) (*config2.Result, error) {
result, err := cmd.devsyUp(ctx, workspaceInfo, tunnelClient)
if err != nil {
return &config2.Result{
Error: err.Error(),
RecoveryAvailable: errors.Is(err, clierr.ErrBuildFailedRecoverable),
}, err
}
// Persist so the daemon, started before the build resolved the
// config, can read the workspace's shutdownAction on the first up.
persistResolvedConfig(workspaceInfo, result)
return result, nil
},
)
if err != nil {
errResult := &config2.Result{
Error: err.Error(),
RecoveryAvailable: errors.Is(err, clierr.ErrBuildFailedRecoverable),
}
if sendErr := cmd.sendResult(ctx, errResult, tunnelClient); sendErr != nil {
log.Errorf("failed to forward up error %q to host: %v", err, sendErr)
}
return err
}

// Persist so the daemon, started before the build resolved the config, can
// read the workspace's shutdownAction on the first up.
persistResolvedConfig(workspaceInfo, result)

// runner.Up can return (result, nil) where result carries a structured
// Error forwarded from the inner container-setup step. Treat that as a
// failure so the agent process exits non-zero and the host doesn't try
// to proceed with a half-populated result.
if err := cmd.sendResult(ctx, result, tunnelClient); err != nil {
return err
}
if result != nil && result.Error != "" {
return fmt.Errorf("%s", result.Error)
}
return nil
}

func (cmd *UpCmd) sendResult(
ctx context.Context,
result *config2.Result,
tunnelClient tunnel.TunnelClient,
) error {
out, err := json.Marshal(result)
if err != nil {
return err
}

_, err = tunnelClient.SendResult(ctx, &tunnel.Message{Message: string(out)})
if err != nil {
return fmt.Errorf("send result: %w", err)
}

return nil
}

func (cmd *UpCmd) devsyUp(
ctx context.Context,
workspaceInfo *provider.AgentWorkspaceInfo,
Expand Down
75 changes: 75 additions & 0 deletions pkg/agent/tunnelserver/result_reporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package tunnelserver

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/devsy-org/devsy/pkg/agent/tunnel"
"github.com/devsy-org/devsy/pkg/devcontainer/config"
"github.com/devsy-org/devsy/pkg/log"
)

const sendResultTimeout = 5 * time.Second

// ReportResult runs fn and guarantees its outcome is sent over the tunnel
// via SendResult exactly once before returning, whether fn succeeds or
// fails.
func ReportResult(
ctx context.Context,
tunnelClient tunnel.TunnelClient,
fn func(ctx context.Context) (*config.Result, error),
) (*config.Result, error) {
result, err := runJob(ctx, fn)

toSend := &config.Result{}
if result != nil {
copied := *result
toSend = &copied
}
if err != nil && toSend.Error == "" {
toSend.Error = err.Error()
}

if sendErr := sendResult(tunnelClient, toSend); sendErr != nil {
if err != nil {
log.Errorf("failed to forward result to host: %v", sendErr)
return result, err
}
return result, sendErr
}

return result, err
}

// runJob recovers a panic in fn so ReportResult's completion guarantee holds
// even when the job crashes outright, instead of the host waiting forever
// for a result that will never arrive.
func runJob(
ctx context.Context,
fn func(ctx context.Context) (*config.Result, error),
) (result *config.Result, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
return fn(ctx)
}

func sendResult(tunnelClient tunnel.TunnelClient, result *config.Result) error {
out, err := json.Marshal(result)
if err != nil {
return err
}

sendCtx, cancel := context.WithTimeout(context.Background(), sendResultTimeout)
defer cancel()

message := &tunnel.Message{Message: string(out)}
if _, err := tunnelClient.SendResult(sendCtx, message); err != nil {
return fmt.Errorf("send result: %w", err)
}
return nil
}
Loading
Loading