diff --git a/cmd/up.go b/cmd/up.go index 0c8c5cb31..64b1ba93c 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -3,26 +3,18 @@ package cmd import ( "context" "fmt" - "io" "os" "os/signal" "path/filepath" "syscall" "github.com/devsy-org/devsy/cmd/flags" - "github.com/devsy-org/devsy/pkg/agent" - "github.com/devsy-org/devsy/pkg/agent/tunnelserver" client2 "github.com/devsy-org/devsy/pkg/client" - "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" config2 "github.com/devsy-org/devsy/pkg/devcontainer/config" - "github.com/devsy-org/devsy/pkg/devcontainer/sshtunnel" - "github.com/devsy-org/devsy/pkg/dotfiles" "github.com/devsy-org/devsy/pkg/ide" - "github.com/devsy-org/devsy/pkg/ide/opener" "github.com/devsy-org/devsy/pkg/log" provider2 "github.com/devsy-org/devsy/pkg/provider" - devssh "github.com/devsy-org/devsy/pkg/ssh" "github.com/devsy-org/devsy/pkg/telemetry" "github.com/devsy-org/devsy/pkg/util" "github.com/spf13/cobra" @@ -219,368 +211,6 @@ func (cmd *UpCmd) executeDevsyUp( return &workspaceContext{result: result, user: user, workdir: workdir}, nil } -// configureWorkspace sets up SSH, Git, and dotfiles. -func (cmd *UpCmd) configureWorkspace( - devsyConfig *config.Config, - client client2.BaseWorkspaceClient, - wctx *workspaceContext, -) error { - if cmd.ConfigureSSH { - devsyHome := "" - if envDevsyHome, ok := os.LookupEnv(config.EnvHome); ok { - devsyHome = envDevsyHome - } - setupGPGAgentForwarding := cmd.GPGAgentForwarding || - devsyConfig.ContextOption(config.ContextOptionGPGAgentForwarding) == config.BoolTrue - sshConfigIncludePath := devsyConfig.ContextOption(config.ContextOptionSSHConfigIncludePath) - - if err := configureSSH(client, configureSSHParams{ - sshConfigPath: cmd.SSHConfigPath, - sshConfigIncludePath: sshConfigIncludePath, - user: wctx.user, - workdir: wctx.workdir, - gpgagent: setupGPGAgentForwarding, - devsyHome: devsyHome, - }); err != nil { - return err - } - - log.Info("SSH configuration completed in workspace") - } - - // Dotfiles are now installed in-container during the lifecycle - // (between postCreateCommand and postStartCommand). The host-side - // SSH-based installation is only used as a fallback when the - // container-side path was not configured. - if cmd.DotfilesRepo == "" { - if err := dotfiles.Setup(dotfiles.SetupParams{ - Source: cmd.DotfilesSource, - Script: cmd.DotfilesScript, - EnvFiles: cmd.DotfilesScriptEnvFile, - EnvKeyValues: cmd.DotfilesScriptEnv, - Client: client, - DevsyConfig: devsyConfig, - }); err != nil { - return err - } - } - - return nil -} - -// openIDE opens the configured IDE. -func (cmd *UpCmd) openIDE( - ctx context.Context, - devsyConfig *config.Config, - client client2.BaseWorkspaceClient, - wctx *workspaceContext, -) error { - if !cmd.OpenIDE { - return nil - } - - ideConfig := client.WorkspaceConfig().IDE - return opener.Open(ctx, ideConfig.Name, ideConfig.Options, opener.Params{ - GPGAgentForwarding: cmd.GPGAgentForwarding, - SSHAuthSockID: cmd.SSHAuthSockID, - GitSSHSigningKey: cmd.GitSSHSigningKey, - DevsyConfig: devsyConfig, - Client: client, - User: wctx.user, - Result: wctx.result, - }) -} - -func (cmd *UpCmd) devsyUp( - ctx context.Context, - devsyConfig *config.Config, - client client2.BaseWorkspaceClient, -) (*config2.Result, error) { - var err error - - // only lock if we are not in platform mode - if !cmd.Platform.Enabled { - err := client.Lock(ctx) - if err != nil { - return nil, fmt.Errorf("lock workspace: %w", err) - } - defer client.Unlock() - } - - // get result - var result *config2.Result - - switch client := client.(type) { - case client2.WorkspaceClient: - result, err = cmd.devsyUpMachine(ctx, devsyConfig, client) - if err != nil { - return nil, err - } - case client2.ProxyClient: - result, err = cmd.devsyUpProxy(ctx, client) - if err != nil { - return nil, err - } - case client2.DaemonClient: - result, err = cmd.devsyUpDaemon(ctx, client) - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("unsupported client type: %T", client) - } - - // save result to file - err = provider2.SaveWorkspaceResult(client.WorkspaceConfig(), result) - if err != nil { - return nil, fmt.Errorf("save workspace result: %w", err) - } - - return result, nil -} - -func (cmd *UpCmd) devsyUpProxy( - ctx context.Context, - client client2.ProxyClient, -) (*config2.Result, error) { - // create pipes - stdoutReader, stdoutWriter, err := os.Pipe() - if err != nil { - return nil, err - } - stdinReader, stdinWriter, err := os.Pipe() - if err != nil { - return nil, err - } - defer func() { _ = stdoutWriter.Close() }() - defer func() { _ = stdinWriter.Close() }() - - // start machine on stdio - cancelCtx, cancel := context.WithCancel(ctx) - defer cancel() - - // create up command - errChan := make(chan error, 1) - go func() { - defer log.Debug("done executing up command") - defer cancel() - - // build devsy up options - workspace := client.WorkspaceConfig() - baseOptions := cmd.CLIOptions - baseOptions.ID = workspace.ID - baseOptions.DevContainerPath = workspace.DevContainerPath - baseOptions.DevContainerImage = workspace.DevContainerImage - baseOptions.IDE = workspace.IDE.Name - baseOptions.IDEOptions = nil - baseOptions.Source = workspace.Source.String() - for optionName, optionValue := range workspace.IDE.Options { - baseOptions.IDEOptions = append( - baseOptions.IDEOptions, - optionName+"="+optionValue.Value, - ) - } - - // run devsy up elsewhere - err = client.Up(ctx, client2.UpOptions{ - CLIOptions: baseOptions, - Debug: cmd.Debug, - - Stdin: stdinReader, - Stdout: stdoutWriter, - }) - if err != nil { - errChan <- fmt.Errorf("executing up proxy command: %w", err) - } else { - errChan <- nil - } - }() - - // create container etc. - result, err := tunnelserver.RunUpServer( - cancelCtx, - stdoutReader, - stdinWriter, - true, - true, - client.WorkspaceConfig(), - ) - if err != nil { - return nil, fmt.Errorf("run tunnel machine: %w", err) - } - - // wait until command finished - return result, <-errChan -} - -func (cmd *UpCmd) devsyUpDaemon( - ctx context.Context, - client client2.DaemonClient, -) (*config2.Result, error) { - // build devsy up options - workspace := client.WorkspaceConfig() - baseOptions := cmd.CLIOptions - baseOptions.ID = workspace.ID - baseOptions.DevContainerPath = workspace.DevContainerPath - baseOptions.DevContainerImage = workspace.DevContainerImage - baseOptions.IDE = workspace.IDE.Name - baseOptions.IDEOptions = nil - baseOptions.Source = workspace.Source.String() - for optionName, optionValue := range workspace.IDE.Options { - baseOptions.IDEOptions = append( - baseOptions.IDEOptions, - optionName+"="+optionValue.Value, - ) - } - - // run devsy up elsewhere - return client.Up(ctx, client2.UpOptions{ - CLIOptions: baseOptions, - Debug: cmd.Debug, - }) -} - -func (cmd *UpCmd) devsyUpMachine( - ctx context.Context, - devsyConfig *config.Config, - client client2.WorkspaceClient, -) (*config2.Result, error) { - err := clientimplementation.StartWait(ctx, client, true) - if err != nil { - return nil, fmt.Errorf("wait for machine: %w", err) - } - - // compress info - workspaceInfo, wInfo, err := client.AgentInfo(cmd.CLIOptions) - if err != nil { - return nil, fmt.Errorf("get agent info: %w", err) - } - - // create container etc. - log.Info("creating devcontainer") - defer log.Debug("done creating devcontainer") - - // if we run on a platform, we need to pass the platform options - if cmd.Platform.Enabled { - return clientimplementation.BuildAgentClient( - ctx, - clientimplementation.BuildAgentClientOptions{ - WorkspaceClient: client, - CLIOptions: cmd.CLIOptions, - AgentCommand: "up", - TunnelOptions: []tunnelserver.Option{ - tunnelserver.WithPlatformOptions(&cmd.Platform), - }, - }, - ) - } - - // ssh tunnel command - sshTunnelCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", client.AgentPath()) - if log.DebugEnabled() { - sshTunnelCmd += " --debug" - } - - // create agent command - agentCommand := fmt.Sprintf( - "'%s' agent workspace up --workspace-info '%s'", - client.AgentPath(), - workspaceInfo, - ) - - if log.DebugEnabled() { - agentCommand += " --debug" - } - - agentInjectFunc := func( - cancelCtx context.Context, sshCmd string, sshTunnelStdinReader, sshTunnelStdoutWriter *os.File, - writer io.WriteCloser, - ) error { - return agent.InjectAgent(&agent.InjectOptions{ - Ctx: cancelCtx, - Exec: func(ctx context.Context, command string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { - return client.Command(ctx, client2.CommandOptions{ - Command: command, - Stdin: stdin, - Stdout: stdout, - Stderr: stderr, - }) - }, - IsLocal: client.AgentLocal(), - RemoteAgentPath: client.AgentPath(), - DownloadURL: client.AgentURL(), - Command: sshCmd, - Stdin: sshTunnelStdinReader, - Stdout: sshTunnelStdoutWriter, - Stderr: writer, - Timeout: wInfo.InjectTimeout, - }) - } - - return sshtunnel.ExecuteCommand(ctx, sshtunnel.ExecuteCommandOptions{ - Client: client, - AddPrivateKeys: devsyConfig.ContextOption( - config.ContextOptionSSHAddPrivateKeys, - ) == config.BoolTrue, - AgentInject: agentInjectFunc, - SSHCommand: sshTunnelCmd, - Command: agentCommand, - TunnelServerFunc: func(ctx context.Context, stdin io.WriteCloser, stdout io.Reader) (*config2.Result, error) { - return tunnelserver.RunUpServer( - ctx, - stdout, - stdin, - client.AgentInjectGitCredentials(cmd.CLIOptions), - client.AgentInjectDockerCredentials(cmd.CLIOptions), - client.WorkspaceConfig(), - ) - }, - }) -} - -type configureSSHParams struct { - sshConfigPath string - sshConfigIncludePath string - user string - workdir string - gpgagent bool - devsyHome string -} - -func configureSSH(client client2.BaseWorkspaceClient, params configureSSHParams) error { - path, err := devssh.ResolveSSHConfigPath(params.sshConfigPath) - if err != nil { - return fmt.Errorf("invalid ssh config path: %w", err) - } - sshConfigPath := path - - sshConfigIncludePath := params.sshConfigIncludePath - if sshConfigIncludePath != "" { - includePath, err := devssh.ResolveSSHConfigPath(sshConfigIncludePath) - if err != nil { - return fmt.Errorf("invalid ssh config include path: %w", err) - } - sshConfigIncludePath = includePath - } - - err = devssh.ConfigureSSHConfig(devssh.SSHConfigParams{ - SSHConfigPath: sshConfigPath, - SSHConfigIncludePath: sshConfigIncludePath, - Context: client.Context(), - Workspace: client.Workspace(), - User: params.user, - Workdir: params.workdir, - GPGAgent: params.gpgagent, - DevsyHome: params.devsyHome, - Provider: client.Provider(), - }) - if err != nil { - return err - } - - return nil -} - func WithSignals(ctx context.Context) (context.Context, func()) { ctx, cancel := context.WithCancel(ctx) signals := make(chan os.Signal, 1) diff --git a/cmd/up_agent.go b/cmd/up_agent.go new file mode 100644 index 000000000..c21de37fb --- /dev/null +++ b/cmd/up_agent.go @@ -0,0 +1,265 @@ +package cmd + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/devsy-org/devsy/pkg/agent" + "github.com/devsy-org/devsy/pkg/agent/tunnelserver" + client2 "github.com/devsy-org/devsy/pkg/client" + "github.com/devsy-org/devsy/pkg/client/clientimplementation" + "github.com/devsy-org/devsy/pkg/config" + config2 "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/devcontainer/sshtunnel" + "github.com/devsy-org/devsy/pkg/log" + provider2 "github.com/devsy-org/devsy/pkg/provider" +) + +func (cmd *UpCmd) devsyUp( //nolint:cyclop + ctx context.Context, + devsyConfig *config.Config, + client client2.BaseWorkspaceClient, +) (*config2.Result, error) { + var err error + + // only lock if we are not in platform mode + if !cmd.Platform.Enabled { + err := client.Lock(ctx) + if err != nil { + return nil, fmt.Errorf("lock workspace: %w", err) + } + defer client.Unlock() + } + + // get result + var result *config2.Result + + switch client := client.(type) { + case client2.WorkspaceClient: + result, err = cmd.devsyUpMachine(ctx, devsyConfig, client) + if err != nil { + return nil, err + } + case client2.ProxyClient: + result, err = cmd.devsyUpProxy(ctx, client) + if err != nil { + return nil, err + } + case client2.DaemonClient: + result, err = cmd.devsyUpDaemon(ctx, client) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unsupported client type: %T", client) + } + + // save result to file + err = provider2.SaveWorkspaceResult(client.WorkspaceConfig(), result) + if err != nil { + return nil, fmt.Errorf("save workspace result: %w", err) + } + + return result, nil +} + +func (cmd *UpCmd) devsyUpProxy( //nolint:funlen + ctx context.Context, + client client2.ProxyClient, +) (*config2.Result, error) { + // create pipes + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return nil, err + } + stdinReader, stdinWriter, err := os.Pipe() + if err != nil { + return nil, err + } + defer func() { _ = stdoutWriter.Close() }() + defer func() { _ = stdinWriter.Close() }() + + // start machine on stdio + cancelCtx, cancel := context.WithCancel(ctx) + defer cancel() + + // create up command + errChan := make(chan error, 1) + go func() { + defer log.Debug("done executing up command") + defer cancel() + + // build devsy up options + workspace := client.WorkspaceConfig() + baseOptions := cmd.CLIOptions + baseOptions.ID = workspace.ID + baseOptions.DevContainerPath = workspace.DevContainerPath + baseOptions.DevContainerImage = workspace.DevContainerImage + baseOptions.IDE = workspace.IDE.Name + baseOptions.IDEOptions = nil + baseOptions.Source = workspace.Source.String() + for optionName, optionValue := range workspace.IDE.Options { + baseOptions.IDEOptions = append( + baseOptions.IDEOptions, + optionName+"="+optionValue.Value, + ) + } + + // run devsy up elsewhere + err = client.Up(ctx, client2.UpOptions{ + CLIOptions: baseOptions, + Debug: cmd.Debug, + + Stdin: stdinReader, + Stdout: stdoutWriter, + }) + if err != nil { + errChan <- fmt.Errorf("executing up proxy command: %w", err) + } else { + errChan <- nil + } + }() + + // create container etc. + result, err := tunnelserver.RunUpServer( + cancelCtx, + stdoutReader, + stdinWriter, + true, + true, + client.WorkspaceConfig(), + ) + if err != nil { + return nil, fmt.Errorf("run tunnel machine: %w", err) + } + + // wait until command finished + return result, <-errChan +} + +func (cmd *UpCmd) devsyUpDaemon( + ctx context.Context, + client client2.DaemonClient, +) (*config2.Result, error) { + // build devsy up options + workspace := client.WorkspaceConfig() + baseOptions := cmd.CLIOptions + baseOptions.ID = workspace.ID + baseOptions.DevContainerPath = workspace.DevContainerPath + baseOptions.DevContainerImage = workspace.DevContainerImage + baseOptions.IDE = workspace.IDE.Name + baseOptions.IDEOptions = nil + baseOptions.Source = workspace.Source.String() + for optionName, optionValue := range workspace.IDE.Options { + baseOptions.IDEOptions = append( + baseOptions.IDEOptions, + optionName+"="+optionValue.Value, + ) + } + + // run devsy up elsewhere + return client.Up(ctx, client2.UpOptions{ + CLIOptions: baseOptions, + Debug: cmd.Debug, + }) +} + +func (cmd *UpCmd) devsyUpMachine( //nolint:funlen + ctx context.Context, + devsyConfig *config.Config, + client client2.WorkspaceClient, +) (*config2.Result, error) { + err := clientimplementation.StartWait(ctx, client, true) + if err != nil { + return nil, fmt.Errorf("wait for machine: %w", err) + } + + // compress info + workspaceInfo, wInfo, err := client.AgentInfo(cmd.CLIOptions) + if err != nil { + return nil, fmt.Errorf("get agent info: %w", err) + } + + // create container etc. + log.Info("creating devcontainer") + defer log.Debug("done creating devcontainer") + + // if we run on a platform, we need to pass the platform options + if cmd.Platform.Enabled { + return clientimplementation.BuildAgentClient( + ctx, + clientimplementation.BuildAgentClientOptions{ + WorkspaceClient: client, + CLIOptions: cmd.CLIOptions, + AgentCommand: "up", + TunnelOptions: []tunnelserver.Option{ + tunnelserver.WithPlatformOptions(&cmd.Platform), + }, + }, + ) + } + + // ssh tunnel command + sshTunnelCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", client.AgentPath()) + if log.DebugEnabled() { + sshTunnelCmd += " --debug" //nolint:goconst + } + + // create agent command + agentCommand := fmt.Sprintf( + "'%s' agent workspace up --workspace-info '%s'", + client.AgentPath(), + workspaceInfo, + ) + + if log.DebugEnabled() { + agentCommand += " --debug" + } + + agentInjectFunc := func( + cancelCtx context.Context, sshCmd string, sshTunnelStdinReader, sshTunnelStdoutWriter *os.File, + writer io.WriteCloser, + ) error { + return agent.InjectAgent(&agent.InjectOptions{ + Ctx: cancelCtx, + Exec: func(ctx context.Context, command string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { + return client.Command(ctx, client2.CommandOptions{ + Command: command, + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + }) + }, + IsLocal: client.AgentLocal(), + RemoteAgentPath: client.AgentPath(), + DownloadURL: client.AgentURL(), + Command: sshCmd, + Stdin: sshTunnelStdinReader, + Stdout: sshTunnelStdoutWriter, + Stderr: writer, + Timeout: wInfo.InjectTimeout, + }) + } + + return sshtunnel.ExecuteCommand(ctx, sshtunnel.ExecuteCommandOptions{ + Client: client, + AddPrivateKeys: devsyConfig.ContextOption( + config.ContextOptionSSHAddPrivateKeys, + ) == config.BoolTrue, + AgentInject: agentInjectFunc, + SSHCommand: sshTunnelCmd, + Command: agentCommand, + TunnelServerFunc: func(ctx context.Context, stdin io.WriteCloser, stdout io.Reader) (*config2.Result, error) { + return tunnelserver.RunUpServer( + ctx, + stdout, + stdin, + client.AgentInjectGitCredentials(cmd.CLIOptions), + client.AgentInjectDockerCredentials(cmd.CLIOptions), + client.WorkspaceConfig(), + ) + }, + }) +} diff --git a/cmd/up_configure.go b/cmd/up_configure.go new file mode 100644 index 000000000..4c3ed6039 --- /dev/null +++ b/cmd/up_configure.go @@ -0,0 +1,129 @@ +package cmd + +import ( + "context" + "fmt" + "os" + + client2 "github.com/devsy-org/devsy/pkg/client" + "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/dotfiles" + "github.com/devsy-org/devsy/pkg/ide/opener" + "github.com/devsy-org/devsy/pkg/log" + devssh "github.com/devsy-org/devsy/pkg/ssh" +) + +// configureWorkspace sets up SSH, Git, and dotfiles. +func (cmd *UpCmd) configureWorkspace( + devsyConfig *config.Config, + client client2.BaseWorkspaceClient, + wctx *workspaceContext, +) error { + if cmd.ConfigureSSH { + devsyHome := "" + if envDevsyHome, ok := os.LookupEnv(config.EnvHome); ok { + devsyHome = envDevsyHome + } + setupGPGAgentForwarding := cmd.GPGAgentForwarding || + devsyConfig.ContextOption(config.ContextOptionGPGAgentForwarding) == config.BoolTrue + sshConfigIncludePath := devsyConfig.ContextOption(config.ContextOptionSSHConfigIncludePath) + + if err := configureSSH(client, configureSSHParams{ + sshConfigPath: cmd.SSHConfigPath, + sshConfigIncludePath: sshConfigIncludePath, + user: wctx.user, + workdir: wctx.workdir, + gpgagent: setupGPGAgentForwarding, + devsyHome: devsyHome, + }); err != nil { + return err + } + + log.Info("SSH configuration completed in workspace") + } + + // Dotfiles are now installed in-container during the lifecycle + // (between postCreateCommand and postStartCommand). The host-side + // SSH-based installation is only used as a fallback when the + // container-side path was not configured. + if cmd.DotfilesRepo == "" { + if err := dotfiles.Setup(dotfiles.SetupParams{ + Source: cmd.DotfilesSource, + Script: cmd.DotfilesScript, + EnvFiles: cmd.DotfilesScriptEnvFile, + EnvKeyValues: cmd.DotfilesScriptEnv, + Client: client, + DevsyConfig: devsyConfig, + }); err != nil { + return err + } + } + + return nil +} + +// openIDE opens the configured IDE. +func (cmd *UpCmd) openIDE( + ctx context.Context, + devsyConfig *config.Config, + client client2.BaseWorkspaceClient, + wctx *workspaceContext, +) error { + if !cmd.OpenIDE { + return nil + } + + ideConfig := client.WorkspaceConfig().IDE + return opener.Open(ctx, ideConfig.Name, ideConfig.Options, opener.Params{ + GPGAgentForwarding: cmd.GPGAgentForwarding, + SSHAuthSockID: cmd.SSHAuthSockID, + GitSSHSigningKey: cmd.GitSSHSigningKey, + DevsyConfig: devsyConfig, + Client: client, + User: wctx.user, + Result: wctx.result, + }) +} + +type configureSSHParams struct { + sshConfigPath string + sshConfigIncludePath string + user string + workdir string + gpgagent bool + devsyHome string +} + +func configureSSH(client client2.BaseWorkspaceClient, params configureSSHParams) error { + path, err := devssh.ResolveSSHConfigPath(params.sshConfigPath) + if err != nil { + return fmt.Errorf("invalid ssh config path: %w", err) + } + sshConfigPath := path + + sshConfigIncludePath := params.sshConfigIncludePath + if sshConfigIncludePath != "" { + includePath, err := devssh.ResolveSSHConfigPath(sshConfigIncludePath) + if err != nil { + return fmt.Errorf("invalid ssh config include path: %w", err) + } + sshConfigIncludePath = includePath + } + + err = devssh.ConfigureSSHConfig(devssh.SSHConfigParams{ + SSHConfigPath: sshConfigPath, + SSHConfigIncludePath: sshConfigIncludePath, + Context: client.Context(), + Workspace: client.Workspace(), + User: params.user, + Workdir: params.workdir, + GPGAgent: params.gpgagent, + DevsyHome: params.devsyHome, + Provider: client.Provider(), + }) + if err != nil { + return err + } + + return nil +}