|
| 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 * as vscode from "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 | throw new Error("You are not logged in") |
394 | 398 | } |
395 | | - await openWorkspace( |
396 | | - baseUrl, |
397 | | - treeItem.workspaceOwner, |
398 | | - treeItem.workspaceName, |
399 | | - treeItem.workspaceAgent, |
400 | | - treeItem.workspaceFolderPath, |
401 | | - true, |
402 | | - ) |
| 399 | + |
| 400 | + let agent = 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 | + await openWorkspace( |
| 409 | + this.restClient, |
| 410 | + baseUrl, |
| 411 | + treeItem.workspaceOwner, |
| 412 | + treeItem.workspaceName, |
| 413 | + agent, |
| 414 | + treeItem.workspaceFolderPath, |
| 415 | + true, |
| 416 | + ) |
| 417 | + } catch (err) { |
| 418 | + const message = 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] as string |
493 | 516 | workspaceName = args[1] as string |
494 | | - // workspaceAgent is reserved for args[2], but multiple agents aren't supported yet. |
| 517 | + workspaceAgent = args[2] as string |
495 | 518 | folderPath = args[3] as string | undefined |
496 | 519 | openRecent = args[4] as boolean | undefined |
497 | 520 | } |
498 | 521 |
|
499 | | - await openWorkspace(baseUrl, workspaceOwner, workspaceName, workspaceAgent, folderPath, openRecent) |
| 522 | + try { |
| 523 | + await openWorkspace( |
| 524 | + this.restClient, |
| 525 | + baseUrl, |
| 526 | + workspaceOwner, |
| 527 | + workspaceName, |
| 528 | + workspaceAgent, |
| 529 | + folderPath, |
| 530 | + openRecent, |
| 531 | + ) |
| 532 | + } catch (err) { |
| 533 | + const message = 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 | async function openWorkspace( |
| 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 | | - const remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent) |
| 599 | + let remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent) |
| 600 | + |
| 601 | + let hostnameSuffix = "coder" |
| 602 | + try { |
| 603 | + const sshConfig = await restClient.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 | + throw error |
| 609 | + } |
| 610 | + switch (error.response?.status) { |
| 611 | + case 404: { |
| 612 | + // Likely a very old deployment, just use the default. |
| 613 | + break |
| 614 | + } |
| 615 | + case 401: { |
| 616 | + throw error |
| 617 | + } |
| 618 | + default: |
| 619 | + throw error |
| 620 | + } |
| 621 | + } |
| 622 | + |
| 623 | + const coderConnectAddr = await maybeCoderConnectAddr(workspaceAgent, workspaceName, workspaceOwner, hostnameSuffix) |
| 624 | + if (coderConnectAddr) { |
| 625 | + remoteAuthority = `ssh-remote+${coderConnectAddr}` |
| 626 | + } |
560 | 627 |
|
561 | 628 | let newWindow = 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 | +async function maybeCoderConnectAddr( |
| 687 | + agent: string, |
| 688 | + workspace: string, |
| 689 | + owner: string, |
| 690 | + hostnameSuffix: string, |
| 691 | +): Promise<string | undefined> { |
| 692 | + const coderConnectHostname = `${agent}.${workspace}.${owner}.${hostnameSuffix}` |
| 693 | + try { |
| 694 | + const res = await promisify(lookup)(coderConnectHostname) |
| 695 | + return res.family == 6 && inRange(res.address, "fd60:627a:a42b::/48") ? coderConnectHostname : undefined |
| 696 | + } catch { |
| 697 | + return undefined |
| 698 | + } |
| 699 | +} |
| 700 | + |
619 | 701 | async function openDevContainer( |
620 | 702 | baseUrl: string, |
621 | 703 | workspaceOwner: string, |
|
0 commit comments