Skip to content

Commit 64aeee3

Browse files
aledbfclaude
andcommitted
fix(config): preserve portsAttributes.protocol and requireLocalPort (spec)
The spec's PortAttributes object (containers.dev json_reference / devContainer.base.schema.json) defines five properties: label, protocol (http|https), onAutoForward, requireLocalPort, elevateIfNeeded. The Go PortAttrs struct only had three, so protocol and requireLocalPort were silently dropped on parse — read-configuration and the devcontainer.metadata label lost them, even though the CLI just needs to pass them through to the editor. Add both fields (covering portsAttributes and otherPortsAttributes). imagemeta already stores portsAttributes as an untyped map, so the metadata path preserves them automatically. Verified via read-configuration round-trip; locked by TestPortAttrsSpecFields. No behavior change for configs without these fields, so parity is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9fcccd4 commit 64aeee3

2 files changed

Lines changed: 58 additions & 4 deletions

File tree

internal/config/types.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,17 @@ type Build struct {
7474
Options []string `json:"options,omitempty"`
7575
}
7676

77-
// PortAttrs represents port attribute configuration.
77+
// PortAttrs represents port attribute configuration. Fields mirror the spec's
78+
// PortAttributes object (containers.dev json_reference): label, protocol
79+
// (http|https), onAutoForward, requireLocalPort, elevateIfNeeded. The CLI does
80+
// not act on these (port forwarding is the editor's job) but must preserve them
81+
// through read-configuration and the devcontainer.metadata label.
7882
type PortAttrs struct {
79-
Label string `json:"label,omitempty"`
80-
OnAutoForward string `json:"onAutoForward,omitempty"`
81-
ElevateIfNeeded *bool `json:"elevateIfNeeded,omitempty"`
83+
Label string `json:"label,omitempty"`
84+
Protocol string `json:"protocol,omitempty"`
85+
OnAutoForward string `json:"onAutoForward,omitempty"`
86+
RequireLocalPort *bool `json:"requireLocalPort,omitempty"`
87+
ElevateIfNeeded *bool `json:"elevateIfNeeded,omitempty"`
8288
}
8389

8490
// HostRequirements specifies minimum host resources.

internal/config/types_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package config
22

33
import (
44
"encoding/json"
5+
"strings"
56
"testing"
67

78
"github.com/devcontainers/cli/internal/jsonc"
@@ -269,3 +270,50 @@ func TestParseFixture_WithJSONC(t *testing.T) {
269270
}
270271

271272
func intPtr(i int) *int { return &i }
273+
274+
// TestPortAttrsSpecFields locks all five spec-defined portsAttributes properties
275+
// (label, protocol, onAutoForward, requireLocalPort, elevateIfNeeded) through a
276+
// parse → re-marshal round-trip. protocol and requireLocalPort were previously
277+
// dropped, silently losing them from read-configuration and the metadata label.
278+
func TestPortAttrsSpecFields(t *testing.T) {
279+
const input = `{
280+
"image": "ubuntu",
281+
"portsAttributes": {
282+
"3000": {"label": "app", "protocol": "https", "onAutoForward": "notify", "requireLocalPort": true, "elevateIfNeeded": false}
283+
},
284+
"otherPortsAttributes": {"onAutoForward": "ignore", "protocol": "http"}
285+
}`
286+
287+
var c DevContainer
288+
if err := json.Unmarshal([]byte(input), &c); err != nil {
289+
t.Fatal(err)
290+
}
291+
292+
pa, ok := c.PortsAttributes["3000"]
293+
if !ok {
294+
t.Fatal("portsAttributes[3000] missing")
295+
}
296+
if pa.Label != "app" || pa.Protocol != "https" || pa.OnAutoForward != "notify" {
297+
t.Errorf("scalars = %+v", pa)
298+
}
299+
if pa.RequireLocalPort == nil || !*pa.RequireLocalPort {
300+
t.Errorf("requireLocalPort = %v, want true", pa.RequireLocalPort)
301+
}
302+
if pa.ElevateIfNeeded == nil || *pa.ElevateIfNeeded {
303+
t.Errorf("elevateIfNeeded = %v, want false", pa.ElevateIfNeeded)
304+
}
305+
if c.OtherPortsAttributes == nil || c.OtherPortsAttributes.Protocol != "http" {
306+
t.Errorf("otherPortsAttributes = %+v", c.OtherPortsAttributes)
307+
}
308+
309+
// Re-marshal must preserve protocol and requireLocalPort (the regression).
310+
out, err := json.Marshal(c)
311+
if err != nil {
312+
t.Fatal(err)
313+
}
314+
for _, want := range []string{`"protocol":"https"`, `"requireLocalPort":true`} {
315+
if !strings.Contains(string(out), want) {
316+
t.Errorf("re-marshaled config dropped %s: %s", want, out)
317+
}
318+
}
319+
}

0 commit comments

Comments
 (0)