|
| 1 | +import{isAxiosError}from"axios" |
1 | 2 | import{Api}from"coder/site/src/api/api"
|
2 | 3 | import{getErrorMessage}from"coder/site/src/api/errors"
|
3 | 4 | import{User,Workspace,WorkspaceAgent}from"coder/site/src/api/typesGenerated"
|
| 5 | +import{lookup}from"dns" |
| 6 | +import{inRange}from"range_check" |
| 7 | +import{promisify}from"util" |
4 | 8 | import*asvscodefrom"vscode"
|
5 | 9 | import{makeCoderSdk,needToken}from"./api"
|
6 | 10 | import{extractAgents}from"./api-helper"
|
@@ -392,14 +396,33 @@ export class Commands {
|
392 | 396 | if(!baseUrl){
|
393 | 397 | thrownewError("You are not logged in")
|
394 | 398 | }
|
395 |
| -awaitopenWorkspace( |
396 |
| -baseUrl, |
397 |
| -treeItem.workspaceOwner, |
398 |
| -treeItem.workspaceName, |
399 |
| -treeItem.workspaceAgent, |
400 |
| -treeItem.workspaceFolderPath, |
401 |
| -true, |
402 |
| -) |
| 399 | + |
| 400 | +letagent=treeItem.workspaceAgent |
| 401 | +if(!agent){ |
| 402 | +// `openFromSidebar` is only callable on agents or single-agent workspaces, |
| 403 | +// where this will always be set. |
| 404 | +return |
| 405 | +} |
| 406 | + |
| 407 | +try{ |
| 408 | +awaitopenWorkspace( |
| 409 | +this.restClient, |
| 410 | +baseUrl, |
| 411 | +treeItem.workspaceOwner, |
| 412 | +treeItem.workspaceName, |
| 413 | +agent, |
| 414 | +treeItem.workspaceFolderPath, |
| 415 | +true, |
| 416 | +) |
| 417 | +}catch(err){ |
| 418 | +constmessage=getErrorMessage(err,"no response from the server") |
| 419 | +this.storage.writeToCoderOutputChannel(`Failed to open workspace:${message}`) |
| 420 | +this.vscodeProposed.window.showErrorMessage("Failed to open workspace",{ |
| 421 | +detail:message, |
| 422 | +modal:true, |
| 423 | +useCustom:true, |
| 424 | +}) |
| 425 | +} |
403 | 426 | }else{
|
404 | 427 | // If there is no tree item, then the user manually ran this command.
|
405 | 428 | // Default to the regular open instead.
|
@@ -491,12 +514,30 @@ export class Commands {
|
491 | 514 | }else{
|
492 | 515 | workspaceOwner=args[0]asstring
|
493 | 516 | workspaceName=args[1]asstring
|
494 |
| -//workspaceAgentis reserved forargs[2], but multiple agents aren't supported yet. |
| 517 | +workspaceAgent=args[2]asstring |
495 | 518 | folderPath=args[3]asstring|undefined
|
496 | 519 | openRecent=args[4]asboolean|undefined
|
497 | 520 | }
|
498 | 521 |
|
499 |
| -awaitopenWorkspace(baseUrl,workspaceOwner,workspaceName,workspaceAgent,folderPath,openRecent) |
| 522 | +try{ |
| 523 | +awaitopenWorkspace( |
| 524 | +this.restClient, |
| 525 | +baseUrl, |
| 526 | +workspaceOwner, |
| 527 | +workspaceName, |
| 528 | +workspaceAgent, |
| 529 | +folderPath, |
| 530 | +openRecent, |
| 531 | +) |
| 532 | +}catch(err){ |
| 533 | +constmessage=getErrorMessage(err,"no response from the server") |
| 534 | +this.storage.writeToCoderOutputChannel(`Failed to open workspace:${message}`) |
| 535 | +this.vscodeProposed.window.showErrorMessage("Failed to open workspace",{ |
| 536 | +detail:message, |
| 537 | +modal:true, |
| 538 | +useCustom:true, |
| 539 | +}) |
| 540 | +} |
500 | 541 | }
|
501 | 542 |
|
502 | 543 | /**
|
@@ -547,16 +588,42 @@ export class Commands {
|
547 | 588 | * both to the Remote SSH plugin in the form of a remote authority URI.
|
548 | 589 | */
|
549 | 590 | asyncfunctionopenWorkspace(
|
| 591 | +restClient:Api, |
550 | 592 | baseUrl:string,
|
551 | 593 | workspaceOwner:string,
|
552 | 594 | workspaceName:string,
|
553 |
| -workspaceAgent:string|undefined, |
| 595 | +workspaceAgent:string, |
554 | 596 | folderPath:string|undefined,
|
555 | 597 | openRecent:boolean|undefined,
|
556 | 598 | ){
|
557 |
| -// A workspace can have multiple agents, but that's handled |
558 |
| -// when opening a workspace unless explicitly specified. |
559 |
| -constremoteAuthority=toRemoteAuthority(baseUrl,workspaceOwner,workspaceName,workspaceAgent) |
| 599 | +letremoteAuthority=toRemoteAuthority(baseUrl,workspaceOwner,workspaceName,workspaceAgent) |
| 600 | + |
| 601 | +lethostnameSuffix="coder" |
| 602 | +try{ |
| 603 | +constsshConfig=awaitrestClient.getDeploymentSSHConfig() |
| 604 | +// If the field is undefined, it's an older server, and always 'coder' |
| 605 | +hostnameSuffix=sshConfig.hostname_suffix??hostnameSuffix |
| 606 | +}catch(error){ |
| 607 | +if(!isAxiosError(error)){ |
| 608 | +throwerror |
| 609 | +} |
| 610 | +switch(error.response?.status){ |
| 611 | +case404:{ |
| 612 | +// Likely a very old deployment, just use the default. |
| 613 | +break |
| 614 | +} |
| 615 | +case401:{ |
| 616 | +throwerror |
| 617 | +} |
| 618 | +default: |
| 619 | +throwerror |
| 620 | +} |
| 621 | +} |
| 622 | + |
| 623 | +constcoderConnectAddr=awaitmaybeCoderConnectAddr(workspaceAgent,workspaceName,workspaceOwner,hostnameSuffix) |
| 624 | +if(coderConnectAddr){ |
| 625 | +remoteAuthority=`ssh-remote+${coderConnectAddr}` |
| 626 | +} |
560 | 627 |
|
561 | 628 | letnewWindow=true
|
562 | 629 | // Open in the existing window if no workspaces are open.
|
@@ -616,6 +683,21 @@ async function openWorkspace(
|
616 | 683 | })
|
617 | 684 | }
|
618 | 685 |
|
| 686 | +asyncfunctionmaybeCoderConnectAddr( |
| 687 | +agent:string, |
| 688 | +workspace:string, |
| 689 | +owner:string, |
| 690 | +hostnameSuffix:string, |
| 691 | +):Promise<string|undefined>{ |
| 692 | +constcoderConnectHostname=`${agent}.${workspace}.${owner}.${hostnameSuffix}` |
| 693 | +try{ |
| 694 | +constres=awaitpromisify(lookup)(coderConnectHostname) |
| 695 | +returnres.family==6&&inRange(res.address,"fd60:627a:a42b::/48") ?coderConnectHostname :undefined |
| 696 | +}catch{ |
| 697 | +returnundefined |
| 698 | +} |
| 699 | +} |
| 700 | + |
619 | 701 | asyncfunctionopenDevContainer(
|
620 | 702 | baseUrl:string,
|
621 | 703 | workspaceOwner:string,
|
|