Skip to content

fix(agent/agentcontainers): generate devcontainer metadata from schema #16881

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 8 commits into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Generated files
agent/agentcontainers/acmock/acmock.go linguist-generated=true
agent/agentcontainers/dcspec/dcspec_gen.go linguist-generated=true
coderd/apidoc/docs.go linguist-generated=true
docs/reference/api/*.md linguist-generated=true
docs/reference/cli/*.md linguist-generated=true
Expand Down
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,8 @@ GEN_FILES := \
examples/examples.gen.json \
$(TAILNETTEST_MOCKS) \
coderd/database/pubsub/psmock/psmock.go \
agent/agentcontainers/acmock/acmock.go

agent/agentcontainers/acmock/acmock.go \
agent/agentcontainers/dcspec/dcspec_gen.go

# all gen targets should be added here and to gen/mark-fresh
gen: gen/db $(GEN_FILES)
Expand Down Expand Up @@ -600,6 +600,7 @@ gen/mark-fresh:
$(TAILNETTEST_MOCKS) \
coderd/database/pubsub/psmock/psmock.go \
agent/agentcontainers/acmock/acmock.go \
agent/agentcontainers/dcspec/dcspec_gen.go \
"

for file in $$files; do
Expand Down Expand Up @@ -634,6 +635,9 @@ coderd/database/pubsub/psmock/psmock.go: coderd/database/pubsub/pubsub.go
agent/agentcontainers/acmock/acmock.go: agent/agentcontainers/containers.go
go generate ./agent/agentcontainers/acmock/

agent/agentcontainers/dcspec/dcspec_gen.go: agent/agentcontainers/dcspec/devContainer.base.schema.json
go generate ./agent/agentcontainers/dcspec/

$(TAILNETTEST_MOCKS): tailnet/coordinator.go tailnet/service.go
go generate ./tailnet/tailnettest/

Expand Down
14 changes: 11 additions & 3 deletions agent/agentcontainers/containers_dockercli.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import (
"strings"
"time"

"github.com/coder/coder/v2/agent/agentcontainers/dcspec"
"github.com/coder/coder/v2/agent/agentexec"
"github.com/coder/coder/v2/agent/usershell"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"

"golang.org/x/exp/maps"
Expand Down Expand Up @@ -183,7 +185,7 @@ func devcontainerEnv(ctx context.Context, execer agentexec.Execer, container str
return nil, nil
}

meta := make([]DevContainerMeta, 0)
meta := make([]dcspec.DevContainer, 0)
if err := json.Unmarshal([]byte(rawMeta), &meta); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: We could first attempt a strict unmarshal via dec := json.NewDecoder(), dec.DisallowUnknownFields(), if it catches an issue we can log it and fall-back to non-strict unmarshaling.

Not strictly necessary, but may help provide hints down the line why something isn't working.

return nil, xerrors.Errorf("unmarshal devcontainer.metadata: %w", err)
}
Expand All @@ -192,7 +194,13 @@ func devcontainerEnv(ctx context.Context, execer agentexec.Execer, container str
env := make([]string, 0)
for _, m := range meta {
for k, v := range m.RemoteEnv {
env = append(env, fmt.Sprintf("%s=%s", k, v))
if v == nil { // *string per spec
Copy link
Member

Choose a reason for hiding this comment

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

I interpret this as env var k should be removed. Does the spec specify more concretely what null means in this context (or do we have to try it out)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately, no. I think we'll have to either try it out or read the implementation.

Copy link
Member

@mafredri mafredri Mar 14, 2025

Choose a reason for hiding this comment

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

It seems @devcontainer/cli has a poor implementation, from:

	"remoteEnv": {
		"FOO": "bar",
		"NODE_OPTIONS": "",
		"GOPRIVATE": null
	},

We get this.

NODE_OPTIONS=
GOPRIVATE=null
FOO=bar

I'm not a fan of the null handling here. But if we want to retain a similar behavior, we shouldn't just skip it.

I'd suggest setting it to an empty string when "none", does that sound reasonable? Or do we want to mimic @devcontainer/cli exactly here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Having a null sneak in is suprising behaviour to me. I'd honestly prefer an empty string, but we can revisit this if it causes compat issues.

// devcontainer-cli will set this to the string "null" if the value is
// not set. Explicitly setting to an empty string here as this would be
// more expected here.
v = ptr.Ref("")
}
env = append(env, fmt.Sprintf("%s=%s", k, *v))
}
}
slices.Sort(env)
Expand Down Expand Up @@ -276,7 +284,7 @@ func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentLi
// log this error, but I'm not sure it's worth it.
ins, dockerInspectStderr, err := runDockerInspect(ctx, dcl.execer, ids...)
if err != nil {
return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("run docker inspect: %w", err)
return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("run docker inspect: %w: %s", err, dockerInspectStderr)
}

for _, in := range ins {
Expand Down
9 changes: 5 additions & 4 deletions agent/agentcontainers/containers_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ import (
// It can be run manually as follows:
//
// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerCLIContainerLister
//
//nolint:paralleltest // This test tends to flake when lots of containers start and stop in parallel.
func TestIntegrationDocker(t *testing.T) {
t.Parallel()
if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" {
t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test")
}
Expand Down Expand Up @@ -418,8 +419,9 @@ func TestConvertDockerVolume(t *testing.T) {
// It can be run manually as follows:
//
// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerEnvInfoer
//
//nolint:paralleltest // This test tends to flake when lots of containers start and stop in parallel.
func TestDockerEnvInfoer(t *testing.T) {
t.Parallel()
if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" {
t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test")
}
Expand Down Expand Up @@ -483,9 +485,8 @@ func TestDockerEnvInfoer(t *testing.T) {
expectedUserShell: "/bin/bash",
},
} {
//nolint:paralleltest // variable recapture no longer required
t.Run(fmt.Sprintf("#%d", idx), func(t *testing.T) {
t.Parallel()

// Start a container with the given image
// and environment variables
image := strings.Split(tt.image, ":")[0]
Expand Down
Loading
Loading