Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitdf3c310

Browse files
authored
feat(cli): addcoder open vscode (#11191)
Fixes#7667
1 parent099be24 commitdf3c310

26 files changed

+1122
-291
lines changed

‎agent/agent_test.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ func TestAgent_EnvironmentVariableExpansion(t *testing.T) {
926926
funcTestAgent_CoderEnvVars(t*testing.T) {
927927
t.Parallel()
928928

929-
for_,key:=range []string{"CODER"} {
929+
for_,key:=range []string{"CODER","CODER_WORKSPACE_NAME","CODER_WORKSPACE_AGENT_NAME"} {
930930
key:=key
931931
t.Run(key,func(t*testing.T) {
932932
t.Parallel()
@@ -2015,6 +2015,12 @@ func setupAgent(t *testing.T, metadata agentsdk.Manifest, ptyTimeout time.Durati
20152015
ifmetadata.AgentID==uuid.Nil {
20162016
metadata.AgentID=uuid.New()
20172017
}
2018+
ifmetadata.AgentName=="" {
2019+
metadata.AgentName="test-agent"
2020+
}
2021+
ifmetadata.WorkspaceName=="" {
2022+
metadata.WorkspaceName="test-workspace"
2023+
}
20182024
coordinator:=tailnet.NewCoordinator(logger)
20192025
t.Cleanup(func() {
20202026
_=coordinator.Close()

‎agent/agentssh/agentssh.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,8 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string)
659659
// Set environment variables reliable detection of being inside a
660660
// Coder workspace.
661661
cmd.Env=append(cmd.Env,"CODER=true")
662+
cmd.Env=append(cmd.Env,"CODER_WORKSPACE_NAME="+manifest.WorkspaceName)
663+
cmd.Env=append(cmd.Env,"CODER_WORKSPACE_AGENT_NAME="+manifest.AgentName)
662664
cmd.Env=append(cmd.Env,fmt.Sprintf("USER=%s",username))
663665
// Git on Windows resolves with UNIX-style paths.
664666
// If using backslashes, it's unable to find the executable.

‎agent/proto/agent.pb.go‎

Lines changed: 295 additions & 274 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎agent/proto/agent.proto‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@ message WorkspaceAgentMetadata {
7575

7676
messageManifest {
7777
bytesagent_id=1;
78+
stringagent_name=15;
7879
stringowner_username=13;
7980
bytesworkspace_id=14;
81+
stringworkspace_name=16;
8082
uint32git_auth_configs=2;
8183
map<string,string>environment_variables=3;
8284
stringdirectory=4;

‎cli/open.go‎

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
"path"
8+
"path/filepath"
9+
"runtime"
10+
"strings"
11+
12+
"github.com/skratchdot/open-golang/open"
13+
"golang.org/x/xerrors"
14+
15+
"github.com/coder/coder/v2/cli/clibase"
16+
"github.com/coder/coder/v2/cli/cliui"
17+
"github.com/coder/coder/v2/codersdk"
18+
)
19+
20+
func (r*RootCmd)open()*clibase.Cmd {
21+
cmd:=&clibase.Cmd{
22+
Use:"open",
23+
Short:"Open a workspace",
24+
Handler:func(inv*clibase.Invocation)error {
25+
returninv.Command.HelpHandler(inv)
26+
},
27+
Children: []*clibase.Cmd{
28+
r.openVSCode(),
29+
},
30+
}
31+
returncmd
32+
}
33+
34+
constvscodeDesktopName="VS Code Desktop"
35+
36+
func (r*RootCmd)openVSCode()*clibase.Cmd {
37+
var (
38+
generateTokenbool
39+
testOpenErrorbool
40+
)
41+
42+
client:=new(codersdk.Client)
43+
cmd:=&clibase.Cmd{
44+
Annotations:workspaceCommand,
45+
Use:"vscode <workspace> [<directory in workspace>]",
46+
Short:fmt.Sprintf("Open a workspace in %s",vscodeDesktopName),
47+
Middleware:clibase.Chain(
48+
clibase.RequireRangeArgs(1,2),
49+
r.InitClient(client),
50+
),
51+
Handler:func(inv*clibase.Invocation)error {
52+
ctx,cancel:=context.WithCancel(inv.Context())
53+
defercancel()
54+
55+
// Check if we're inside a workspace, and especially inside _this_
56+
// workspace so we can perform path resolution/expansion. Generally,
57+
// we know that if we're inside a workspace, `open` can't be used.
58+
insideAWorkspace:=inv.Environ.Get("CODER")=="true"
59+
inWorkspaceName:=inv.Environ.Get("CODER_WORKSPACE_NAME")+"."+inv.Environ.Get("CODER_WORKSPACE_AGENT_NAME")
60+
61+
// We need a started workspace to figure out e.g. expanded directory.
62+
// Pehraps the vscode-coder extension could handle this by accepting
63+
// default_directory=true, then probing the agent. Then we wouldn't
64+
// need to wait for the agent to start.
65+
workspaceQuery:=inv.Args[0]
66+
autostart:=true
67+
workspace,workspaceAgent,err:=getWorkspaceAndAgent(ctx,inv,client,autostart,codersdk.Me,workspaceQuery)
68+
iferr!=nil {
69+
returnxerrors.Errorf("get workspace and agent: %w",err)
70+
}
71+
72+
workspaceName:=workspace.Name+"."+workspaceAgent.Name
73+
insideThisWorkspace:=insideAWorkspace&&inWorkspaceName==workspaceName
74+
75+
if!insideThisWorkspace {
76+
// Wait for the agent to connect, we don't care about readiness
77+
// otherwise (e.g. wait).
78+
err=cliui.Agent(ctx,inv.Stderr,workspaceAgent.ID, cliui.AgentOptions{
79+
Fetch:client.WorkspaceAgent,
80+
FetchLogs:nil,
81+
Wait:false,
82+
})
83+
iferr!=nil {
84+
ifxerrors.Is(err,context.Canceled) {
85+
returncliui.Canceled
86+
}
87+
returnxerrors.Errorf("agent: %w",err)
88+
}
89+
90+
// The agent will report it's expanded directory before leaving
91+
// the created state, so we need to wait for that to happen.
92+
// However, if no directory is set, the expanded directory will
93+
// not be set either.
94+
ifworkspaceAgent.Directory!="" {
95+
workspace,workspaceAgent,err=waitForAgentCond(ctx,client,workspace,workspaceAgent,func(a codersdk.WorkspaceAgent)bool {
96+
returnworkspaceAgent.LifecycleState!=codersdk.WorkspaceAgentLifecycleCreated
97+
})
98+
iferr!=nil {
99+
returnxerrors.Errorf("wait for agent: %w",err)
100+
}
101+
}
102+
}
103+
104+
vardirectorystring
105+
iflen(inv.Args)>1 {
106+
directory=inv.Args[1]
107+
}
108+
directory,err=resolveAgentAbsPath(workspaceAgent.ExpandedDirectory,directory,workspaceAgent.OperatingSystem,insideThisWorkspace)
109+
iferr!=nil {
110+
returnxerrors.Errorf("resolve agent path: %w",err)
111+
}
112+
113+
u:=&url.URL{
114+
Scheme:"vscode",
115+
Host:"coder.coder-remote",
116+
Path:"/open",
117+
}
118+
119+
qp:= url.Values{}
120+
121+
qp.Add("url",client.URL.String())
122+
qp.Add("owner",workspace.OwnerName)
123+
qp.Add("workspace",workspace.Name)
124+
qp.Add("agent",workspaceAgent.Name)
125+
ifdirectory!="" {
126+
qp.Add("folder",directory)
127+
}
128+
129+
// We always set the token if we believe we can open without
130+
// printing the URI, otherwise the token must be explicitly
131+
// requested as it will be printed in plain text.
132+
if!insideAWorkspace||generateToken {
133+
// Prepare an API key. This is for automagical configuration of
134+
// VS Code, however, if running on a local machine we could try
135+
// to probe VS Code settings to see if the current configuration
136+
// is valid. Future improvement idea.
137+
apiKey,err:=client.CreateAPIKey(ctx,codersdk.Me)
138+
iferr!=nil {
139+
returnxerrors.Errorf("create API key: %w",err)
140+
}
141+
qp.Add("token",apiKey.Key)
142+
}
143+
144+
u.RawQuery=qp.Encode()
145+
146+
openingPath:=workspaceName
147+
ifdirectory!="" {
148+
openingPath+=":"+directory
149+
}
150+
151+
ifinsideAWorkspace {
152+
_,_=fmt.Fprintf(inv.Stderr,"Opening %s in %s is not supported inside a workspace, please open the following URI on your local machine instead:\n\n",openingPath,vscodeDesktopName)
153+
_,_=fmt.Fprintf(inv.Stdout,"%s\n",u.String())
154+
returnnil
155+
}
156+
_,_=fmt.Fprintf(inv.Stderr,"Opening %s in %s\n",openingPath,vscodeDesktopName)
157+
158+
if!testOpenError {
159+
err=open.Run(u.String())
160+
}else {
161+
err=xerrors.New("test.open-error")
162+
}
163+
iferr!=nil {
164+
if!generateToken {
165+
// This is not an important step, so we don't want
166+
// to block the user here.
167+
token:=qp.Get("token")
168+
wait:=doAsync(func() {
169+
// Best effort, we don't care if this fails.
170+
apiKeyID:=strings.SplitN(token,"-",2)[0]
171+
_=client.DeleteAPIKey(ctx,codersdk.Me,apiKeyID)
172+
})
173+
deferwait()
174+
175+
qp.Del("token")
176+
u.RawQuery=qp.Encode()
177+
}
178+
179+
_,_=fmt.Fprintf(inv.Stderr,"Could not automatically open %s in %s: %s\n",openingPath,vscodeDesktopName,err)
180+
_,_=fmt.Fprintf(inv.Stderr,"Please open the following URI instead:\n\n")
181+
_,_=fmt.Fprintf(inv.Stdout,"%s\n",u.String())
182+
returnnil
183+
}
184+
185+
returnnil
186+
},
187+
}
188+
189+
cmd.Options= clibase.OptionSet{
190+
{
191+
Flag:"generate-token",
192+
Env:"CODER_OPEN_VSCODE_GENERATE_TOKEN",
193+
Description:fmt.Sprintf(
194+
"Generate an auth token and include it in the vscode:// URI. This is for automagical configuration of %s and not needed if already configured. "+
195+
"This flag does not need to be specified when running this command on a local machine unless automatic open fails.",
196+
vscodeDesktopName,
197+
),
198+
Value:clibase.BoolOf(&generateToken),
199+
},
200+
{
201+
Flag:"test.open-error",
202+
Description:"Don't run the open command.",
203+
Value:clibase.BoolOf(&testOpenError),
204+
Hidden:true,// This is for testing!
205+
},
206+
}
207+
208+
returncmd
209+
}
210+
211+
// waitForAgentCond uses the watch workspace API to update the agent information
212+
// until the condition is met.
213+
funcwaitForAgentCond(ctx context.Context,client*codersdk.Client,workspace codersdk.Workspace,workspaceAgent codersdk.WorkspaceAgent,condfunc(codersdk.WorkspaceAgent)bool) (codersdk.Workspace, codersdk.WorkspaceAgent,error) {
214+
ctx,cancel:=context.WithCancel(ctx)
215+
defercancel()
216+
217+
ifcond(workspaceAgent) {
218+
returnworkspace,workspaceAgent,nil
219+
}
220+
221+
wc,err:=client.WatchWorkspace(ctx,workspace.ID)
222+
iferr!=nil {
223+
returnworkspace,workspaceAgent,xerrors.Errorf("watch workspace: %w",err)
224+
}
225+
226+
forworkspace=rangewc {
227+
workspaceAgent,err=getWorkspaceAgent(workspace,workspaceAgent.Name)
228+
iferr!=nil {
229+
returnworkspace,workspaceAgent,xerrors.Errorf("get workspace agent: %w",err)
230+
}
231+
ifcond(workspaceAgent) {
232+
returnworkspace,workspaceAgent,nil
233+
}
234+
}
235+
236+
returnworkspace,workspaceAgent,xerrors.New("watch workspace: unexpected closed channel")
237+
}
238+
239+
// isWindowsAbsPath does a simplistic check for if the path is an absolute path
240+
// on Windows. Drive letter or preceding `\` is interpreted as absolute.
241+
funcisWindowsAbsPath(pstring)bool {
242+
// Remove the drive letter, if present.
243+
iflen(p)>=2&&p[1]==':' {
244+
p=p[2:]
245+
}
246+
247+
switch {
248+
caselen(p)==0:
249+
returnfalse
250+
casep[0]=='\\':
251+
returntrue
252+
default:
253+
returnfalse
254+
}
255+
}
256+
257+
// windowsJoinPath joins the elements into a path, using Windows path separator
258+
// and converting forward slashes to backslashes.
259+
funcwindowsJoinPath(elem...string)string {
260+
ifruntime.GOOS=="windows" {
261+
returnfilepath.Join(elem...)
262+
}
263+
264+
varsstring
265+
for_,e:=rangeelem {
266+
e=unixToWindowsPath(e)
267+
ife=="" {
268+
continue
269+
}
270+
ifs=="" {
271+
s=e
272+
continue
273+
}
274+
s+="\\"+strings.TrimSuffix(e,"\\")
275+
}
276+
returns
277+
}
278+
279+
funcunixToWindowsPath(pstring)string {
280+
returnstrings.ReplaceAll(p,"/","\\")
281+
}
282+
283+
// resolveAgentAbsPath resolves the absolute path to a file or directory in the
284+
// workspace. If the path is relative, it will be resolved relative to the
285+
// workspace's expanded directory. If the path is absolute, it will be returned
286+
// as-is. If the path is relative and the workspace directory is not expanded,
287+
// an error will be returned.
288+
//
289+
// If the path is being resolved within the workspace, the path will be resolved
290+
// relative to the current working directory.
291+
funcresolveAgentAbsPath(workingDirectory,relOrAbsPath,agentOSstring,localbool) (string,error) {
292+
switch {
293+
caserelOrAbsPath=="":
294+
returnworkingDirectory,nil
295+
296+
caserelOrAbsPath=="~"||strings.HasPrefix(relOrAbsPath,"~/"):
297+
return"",xerrors.Errorf("path %q requires expansion and is not supported, use an absolute path instead",relOrAbsPath)
298+
299+
caselocal:
300+
p,err:=filepath.Abs(relOrAbsPath)
301+
iferr!=nil {
302+
return"",xerrors.Errorf("expand path: %w",err)
303+
}
304+
returnp,nil
305+
306+
caseagentOS=="windows":
307+
relOrAbsPath=unixToWindowsPath(relOrAbsPath)
308+
switch {
309+
caseworkingDirectory!=""&&!isWindowsAbsPath(relOrAbsPath):
310+
returnwindowsJoinPath(workingDirectory,relOrAbsPath),nil
311+
caseisWindowsAbsPath(relOrAbsPath):
312+
returnrelOrAbsPath,nil
313+
default:
314+
return"",xerrors.Errorf("path %q not supported, use an absolute path instead",relOrAbsPath)
315+
}
316+
317+
// Note that we use `path` instead of `filepath` since we want Unix behavior.
318+
caseworkingDirectory!=""&&!path.IsAbs(relOrAbsPath):
319+
returnpath.Join(workingDirectory,relOrAbsPath),nil
320+
casepath.IsAbs(relOrAbsPath):
321+
returnrelOrAbsPath,nil
322+
default:
323+
return"",xerrors.Errorf("path %q not supported, use an absolute path instead",relOrAbsPath)
324+
}
325+
}
326+
327+
funcdoAsync(ffunc()) (waitfunc()) {
328+
done:=make(chanstruct{})
329+
gofunc() {
330+
deferclose(done)
331+
f()
332+
}()
333+
returnfunc() {
334+
<-done
335+
}
336+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp