Skip to content

Control automatic port forwarding with a devcontainer.json file #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 27, 2025

Conversation

aaronlehmann
Copy link
Contributor

devcontainer.json has a system for specifying default behavior for forwarding ports, and also behavior for specific ports and ranges of ports. Its schema is essentially identical to the settings VS Code uses to control port forwarding, so supporting this format for port settings keeps things consistent between VS Code and JetBrains.

See https://containers.dev/implementors/json_reference/ for the spec. As an example, this will turn off automatic port forwarding except for ports 7123 and 8100-8150:

{
  "otherPortsAttributes": {
    "onAutoForward": "ignore"
  },
  "portsAttributes": {
    "7123": {
      "onAutoForward": "notify"
    },
    "8100-8150": {
      "onAutoForward": "notify"
    }
  }
}

Fixes: #38

@@ -48,12 +56,44 @@ class CoderPortForwardService(
}

private fun start() {
logger.info("starting port scanner")
// TODO: make path configurable?
val devcontainerFile = File(System.getProperty("user.home"), ".cache/JetBrains/devcontainer.json")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally wanted to make this a setting, but this turns out to be a bit complicated because this plugin doesn't have any settings yet, so there's a bit of infrastructure involved. I can come back to this if needed, or we can go with this fixed path for now (or some other path). It's pretty straightforward for other software on the workspace to put the file in the expected place or create a symlink.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a fixed path for now is fine, and then we'll see if a need for a setting arises.

Nit: Might be worth extracting to a function in some kind of config module just to have a natural place to start moving things that could become settings, but definitely doesn't need to happen in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to a settings class, please let me know if the location/naming makes sense.

Copy link
Collaborator

@bcpeinhardt bcpeinhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving as all my comments are actually nits. I like the approach here, especially that it provides consistency with VSCode.

Side note: Everybody has different preferences on the level on "nit" comments they want to see in reviews, so let me know if these bug you. I mainly don't want you to see a blanket approval on a PR and think "Ben hardly read this", but I also don't want to hold up progress unless there's actually a good reason too :)


return when {
// Try parsing as single port
portPart.all { it.isDigit() } -> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non Blocking Nit/Question: Should we validate the port is between 0 and 65535 for the SinglePort and PortRange parsing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this validation

@@ -48,12 +56,44 @@ class CoderPortForwardService(
}

private fun start() {
logger.info("starting port scanner")
// TODO: make path configurable?
val devcontainerFile = File(System.getProperty("user.home"), ".cache/JetBrains/devcontainer.json")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a fixed path for now is fine, and then we'll see if a need for a setting arises.

Nit: Might be worth extracting to a function in some kind of config module just to have a natural place to start moving things that could become settings, but definitely doesn't need to happen in this PR.

val portsAttributes = obj.optJSONObject("portsAttributes") ?: JSONObject()
portsAttributes.keys().forEach { spec ->
portsAttributes.optJSONObject(spec)?.let { attrs ->
val onAutoForward = attrs.optString("onAutoForward")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit/Not even sure I want a change here: spec says "onAutoForward" is an an enum of [ "notify", "openBrowser", "openBrowserOnce", "openPreview", "silent", "ignore" ]. Might be worth checking we don't set a PortMatcher('[object Object]') or something to save us a debugging headache in the future, but also see the value of leaving it flexible so we don't have to come back and make an update in the event the spec changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added validation that the onAutoForward value is a string. I think it probably makes sense to accept any non-empty value here except ignore as equivalent to notify, but happy to tweak this as you see fit.

@codecov-commenter
Copy link

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

devcontainer.json has a system for specifying default behavior for
forwarding ports, and also behavior for specific ports and ranges of
ports. Its schema is essentially identical to the settings VS Code uses
to control port forwarding, so supporting this format for port settings
keeps things consistent between VS Code and JetBrains.

See https://containers.dev/implementors/json_reference/ for the spec.
As an example, this will turn off automatic port forwarding except for
ports 7123 and 8100-8150:

    {
      "otherPortsAttributes": {
        "onAutoForward": "ignore"
      },
      "portsAttributes": {
        "7123": {
          "onAutoForward": "notify"
        },
        "8100-8150": {
          "onAutoForward": "notify"
        }
      }
    }

Fixes: coder#38
@aaronlehmann aaronlehmann force-pushed the ports-from-devcontainer-json branch from 8886009 to 3433d28 Compare February 26, 2025 01:17
@@ -24,6 +25,7 @@ repositories {
// Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog
dependencies {
// implementation(libs.annotations)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
Copy link
Collaborator

@fioan89 fioan89 Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for changing this.

Since IntelliJ provides it at runtime as well this should be compileOnly. Another important thing is that here we need to declare the version that is packaged with minimum supported IntelliJ.
We support IntelliJ 2022.3 which uses kotlinx.serialization:1.4.1 so that's the minimum version we should use, and it should only be changed when the minimum supported version of IntelliJ changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's unfortunate because this version is too old to support allowComments, and devcontainer.json files often include comments (since the reference implementation and VS Code allow them). It's not an issue in my particular case because I'll be stripping comments before injecting this file, but for maximum compatibility it seems like supporting comments would be a good thing.

Could we depend on this newer version and add a comment that the dependency can be changed to compileOnly once the minimum supported IDE version provides kotlinx.serialization >= 1.7.0? It looks like that minimum IDE version will be idea/251.14649.49 (which includes JetBrains/intellij-community@548a3c9).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as I know it should be possible to shadow the dependencies but please test it. As a rule of thumb is best not to shadow intellij dependencies to avoid any classloader/memory leak issue.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some context: any jars packaged with the plugin are not exposed to the rest of the IDE or other plugins, the plugin has its own classloader, so it's a good way to make plugin stable in its sandbox

So if we need this serialization library and there's a compatibility issue, it's better to bundle it.

Copy link
Contributor Author

@aaronlehmann aaronlehmann Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the comment about switching to compileOnly in the future. I've been testing with IntelliJ 242.23339.11 and it seems to work fine, but just tried with EAP 251.22821.72 and it works with that version as well.

Let me know if you think it's better or safer to downgrade the dependency and remove allowComments. I'm also fine with that approach.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the plugin has its own classloader, so it's a good way to make plugin stable in its sandbox

Correct , and intellij platform sdk also allows the plugins to reference classes provided by other plugins in which the classloader for those plugin dependencies will be used. Not really the case here, so I agree with your statement. It should be safe to pack the serialization library.

Let me know if you think it's better or safer to downgrade the dependency and remove allowComments

I agree with @kirillk there is no need for a downgrade for now.

@aaronlehmann
Copy link
Contributor Author

@bcpeinhardt @fioan89: Thanks for the reviews! Are there any next steps to get this merged?

@fioan89
Copy link
Collaborator

fioan89 commented Feb 27, 2025

I don't have any other concern @aaronlehmann

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature: Dynamic port forwarding allow+deny list
5 participants