|
| 1 | +package cli |
| 2 | + |
| 3 | +import ( |
| 4 | +"context" |
| 5 | +"fmt" |
| 6 | +"net/url" |
| 7 | +"strings" |
| 8 | + |
| 9 | +"github.com/skratchdot/open-golang/open" |
| 10 | +"golang.org/x/xerrors" |
| 11 | + |
| 12 | +"github.com/coder/coder/v2/cli/clibase" |
| 13 | +"github.com/coder/coder/v2/cli/cliui" |
| 14 | +"github.com/coder/coder/v2/codersdk" |
| 15 | +) |
| 16 | + |
| 17 | +func (r*RootCmd)open()*clibase.Cmd { |
| 18 | +cmd:=&clibase.Cmd{ |
| 19 | +Use:"open", |
| 20 | +Short:"Open a workspace", |
| 21 | +Handler:func(inv*clibase.Invocation)error { |
| 22 | +returninv.Command.HelpHandler(inv) |
| 23 | +}, |
| 24 | +Children: []*clibase.Cmd{ |
| 25 | +r.openVSCode(), |
| 26 | +}, |
| 27 | +} |
| 28 | +returncmd |
| 29 | +} |
| 30 | + |
| 31 | +func (r*RootCmd)openVSCode()*clibase.Cmd { |
| 32 | +vartestNoOpenbool |
| 33 | + |
| 34 | +client:=new(codersdk.Client) |
| 35 | +cmd:=&clibase.Cmd{ |
| 36 | +Annotations:workspaceCommand, |
| 37 | +Use:"vscode <workspace> [<directory in workspace>]", |
| 38 | +Short:"Open a workspace in Visual Studio Code", |
| 39 | +Middleware:clibase.Chain( |
| 40 | +clibase.RequireRangeArgs(1,-1), |
| 41 | +r.InitClient(client), |
| 42 | +), |
| 43 | +Handler:func(inv*clibase.Invocation)error { |
| 44 | +ctx,cancel:=context.WithCancel(inv.Context()) |
| 45 | +defercancel() |
| 46 | + |
| 47 | +// Prepare an API key. This is for automagical configuration of |
| 48 | +// VS Code, however, we could try to probe VS Code settings to see |
| 49 | +// if the current configuration is valid. Future improvement idea. |
| 50 | +apiKey,err:=client.CreateAPIKey(ctx,codersdk.Me) |
| 51 | +iferr!=nil { |
| 52 | +returnxerrors.Errorf("create API key: %w",err) |
| 53 | +} |
| 54 | + |
| 55 | +// We need a started workspace to figure out e.g. expanded directory. |
| 56 | +// Pehraps the vscode-coder extension could handle this by accepting |
| 57 | +// default_directory=true, then probing the agent. Then we wouldn't |
| 58 | +// need to wait for the agent to start. |
| 59 | +workspaceName:=inv.Args[0] |
| 60 | +autostart:=true |
| 61 | +workspace,workspaceAgent,err:=getWorkspaceAndAgent(ctx,inv,client,autostart,codersdk.Me,workspaceName) |
| 62 | +iferr!=nil { |
| 63 | +returnxerrors.Errorf("get workspace and agent: %w",err) |
| 64 | +} |
| 65 | + |
| 66 | +// We could optionally add a flag to skip wait, like with SSH. |
| 67 | +wait:=false |
| 68 | +for_,script:=rangeworkspaceAgent.Scripts { |
| 69 | +ifscript.StartBlocksLogin { |
| 70 | +wait=true |
| 71 | +break |
| 72 | +} |
| 73 | +} |
| 74 | +err=cliui.Agent(ctx,inv.Stderr,workspaceAgent.ID, cliui.AgentOptions{ |
| 75 | +Fetch:client.WorkspaceAgent, |
| 76 | +FetchLogs:client.WorkspaceAgentLogsAfter, |
| 77 | +Wait:wait, |
| 78 | +}) |
| 79 | +iferr!=nil { |
| 80 | +ifxerrors.Is(err,context.Canceled) { |
| 81 | +returncliui.Canceled |
| 82 | +} |
| 83 | +returnxerrors.Errorf("agent: %w",err) |
| 84 | +} |
| 85 | + |
| 86 | +// If the ExpandedDirectory was initially missing, it could mean |
| 87 | +// that the agent hadn't reported it in yet. Retry once. |
| 88 | +ifworkspaceAgent.ExpandedDirectory=="" { |
| 89 | +autostart=false// Don't retry autostart. |
| 90 | +workspace,workspaceAgent,err=getWorkspaceAndAgent(ctx,inv,client,autostart,codersdk.Me,workspaceName) |
| 91 | +iferr!=nil { |
| 92 | +returnxerrors.Errorf("get workspace and agent retry: %w",err) |
| 93 | +} |
| 94 | +} |
| 95 | + |
| 96 | +varfolderstring |
| 97 | +switch { |
| 98 | +caselen(inv.Args)>1: |
| 99 | +folder=inv.Args[1] |
| 100 | +// Perhaps we could SSH in to expand the directory? |
| 101 | +ifstrings.HasPrefix(folder,"~") { |
| 102 | +returnxerrors.Errorf("folder path %q not supported, use an absolute path instead",folder) |
| 103 | +} |
| 104 | +caseworkspaceAgent.ExpandedDirectory!="": |
| 105 | +folder=workspaceAgent.ExpandedDirectory |
| 106 | +} |
| 107 | + |
| 108 | +qp:= url.Values{} |
| 109 | + |
| 110 | +qp.Add("url",client.URL.String()) |
| 111 | +qp.Add("token",apiKey.Key) |
| 112 | +qp.Add("owner",workspace.OwnerName) |
| 113 | +qp.Add("workspace",workspace.Name) |
| 114 | +qp.Add("agent",workspaceAgent.Name) |
| 115 | +iffolder!="" { |
| 116 | +qp.Add("folder",folder) |
| 117 | +} |
| 118 | + |
| 119 | +uri:=fmt.Sprintf("vscode://coder.coder-remote/open?%s",qp.Encode()) |
| 120 | +_,_=fmt.Fprintf(inv.Stdout,"Opening %s\n",strings.ReplaceAll(uri,apiKey.Key,"<REDACTED>")) |
| 121 | + |
| 122 | +iftestNoOpen { |
| 123 | +returnnil |
| 124 | +} |
| 125 | + |
| 126 | +err=open.Run(uri) |
| 127 | +iferr!=nil { |
| 128 | +returnxerrors.Errorf("open: %w",err) |
| 129 | +} |
| 130 | + |
| 131 | +returnnil |
| 132 | +}, |
| 133 | +} |
| 134 | + |
| 135 | +cmd.Options= clibase.OptionSet{ |
| 136 | +{ |
| 137 | +Flag:"test.no-open", |
| 138 | +Description:"Don't run the open command.", |
| 139 | +Value:clibase.BoolOf(&testNoOpen), |
| 140 | +Hidden:true,// This is for testing! |
| 141 | +}, |
| 142 | +} |
| 143 | + |
| 144 | +returncmd |
| 145 | +} |