|
| 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, |
|