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