Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit3c4d920

Browse files
authored
feat(agent/agentcontainers): add feature options as envs (#18576)
1 parent688d2ee commit3c4d920

File tree

4 files changed

+282
-6
lines changed

4 files changed

+282
-6
lines changed

‎agent/agentcontainers/api.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,7 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
13021302
}
13031303

13041304
var (
1305+
featureOptionsAsEnvs []string
13051306
appsWithPossibleDuplicates []SubAgentApp
13061307
workspaceFolder=DevcontainerDefaultContainerWorkspaceFolder
13071308
)
@@ -1313,12 +1314,14 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
13131314
)
13141315

13151316
readConfig:=func() (DevcontainerConfig,error) {
1316-
returnapi.dccli.ReadConfig(ctx,dc.WorkspaceFolder,dc.ConfigPath, []string{
1317-
fmt.Sprintf("CODER_WORKSPACE_AGENT_NAME=%s",subAgentConfig.Name),
1318-
fmt.Sprintf("CODER_WORKSPACE_OWNER_NAME=%s",api.ownerName),
1319-
fmt.Sprintf("CODER_WORKSPACE_NAME=%s",api.workspaceName),
1320-
fmt.Sprintf("CODER_URL=%s",api.subAgentURL),
1321-
})
1317+
returnapi.dccli.ReadConfig(ctx,dc.WorkspaceFolder,dc.ConfigPath,
1318+
append(featureOptionsAsEnvs, []string{
1319+
fmt.Sprintf("CODER_WORKSPACE_AGENT_NAME=%s",subAgentConfig.Name),
1320+
fmt.Sprintf("CODER_WORKSPACE_OWNER_NAME=%s",api.ownerName),
1321+
fmt.Sprintf("CODER_WORKSPACE_NAME=%s",api.workspaceName),
1322+
fmt.Sprintf("CODER_URL=%s",api.subAgentURL),
1323+
}...),
1324+
)
13221325
}
13231326

13241327
ifconfig,err=readConfig();err!=nil {
@@ -1334,6 +1337,11 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
13341337

13351338
workspaceFolder=config.Workspace.WorkspaceFolder
13361339

1340+
featureOptionsAsEnvs=config.MergedConfiguration.Features.OptionsAsEnvs()
1341+
iflen(featureOptionsAsEnvs)>0 {
1342+
configOutdated=true
1343+
}
1344+
13371345
// NOTE(DanielleMaywood):
13381346
// We only want to take an agent name specified in the root customization layer.
13391347
// This restricts the ability for a feature to specify the agent name. We may revisit

‎agent/agentcontainers/api_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2060,6 +2060,122 @@ func TestAPI(t *testing.T) {
20602060
require.Len(t,fSAC.created,1)
20612061
})
20622062

2063+
t.Run("ReadConfigWithFeatureOptions",func(t*testing.T) {
2064+
t.Parallel()
2065+
2066+
ifruntime.GOOS=="windows" {
2067+
t.Skip("Dev Container tests are not supported on Windows (this test uses mocks but fails due to Windows paths)")
2068+
}
2069+
2070+
var (
2071+
ctx=testutil.Context(t,testutil.WaitMedium)
2072+
logger=testutil.Logger(t)
2073+
mClock=quartz.NewMock(t)
2074+
mCCLI=acmock.NewMockContainerCLI(gomock.NewController(t))
2075+
fSAC=&fakeSubAgentClient{
2076+
logger:logger.Named("fakeSubAgentClient"),
2077+
createErrC:make(chanerror,1),
2078+
}
2079+
fDCCLI=&fakeDevcontainerCLI{
2080+
readConfig: agentcontainers.DevcontainerConfig{
2081+
MergedConfiguration: agentcontainers.DevcontainerMergedConfiguration{
2082+
Features: agentcontainers.DevcontainerFeatures{
2083+
"./code-server":map[string]any{
2084+
"port":9090,
2085+
},
2086+
"ghcr.io/devcontainers/features/docker-in-docker:2":map[string]any{
2087+
"moby":"false",
2088+
},
2089+
},
2090+
},
2091+
Workspace: agentcontainers.DevcontainerWorkspace{
2092+
WorkspaceFolder:"/workspaces/coder",
2093+
},
2094+
},
2095+
readConfigErrC:make(chanfunc(envs []string)error,2),
2096+
}
2097+
2098+
testContainer= codersdk.WorkspaceAgentContainer{
2099+
ID:"test-container-id",
2100+
FriendlyName:"test-container",
2101+
Image:"test-image",
2102+
Running:true,
2103+
CreatedAt:time.Now(),
2104+
Labels:map[string]string{
2105+
agentcontainers.DevcontainerLocalFolderLabel:"/workspaces/coder",
2106+
agentcontainers.DevcontainerConfigFileLabel:"/workspaces/coder/.devcontainer/devcontainer.json",
2107+
},
2108+
}
2109+
)
2110+
2111+
coderBin,err:=os.Executable()
2112+
require.NoError(t,err)
2113+
2114+
// Mock the `List` function to always return our test container.
2115+
mCCLI.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{
2116+
Containers: []codersdk.WorkspaceAgentContainer{testContainer},
2117+
},nil).AnyTimes()
2118+
2119+
// Mock the steps used for injecting the coder agent.
2120+
gomock.InOrder(
2121+
mCCLI.EXPECT().DetectArchitecture(gomock.Any(),testContainer.ID).Return(runtime.GOARCH,nil),
2122+
mCCLI.EXPECT().ExecAs(gomock.Any(),testContainer.ID,"root","mkdir","-p","/.coder-agent").Return(nil,nil),
2123+
mCCLI.EXPECT().Copy(gomock.Any(),testContainer.ID,coderBin,"/.coder-agent/coder").Return(nil),
2124+
mCCLI.EXPECT().ExecAs(gomock.Any(),testContainer.ID,"root","chmod","0755","/.coder-agent","/.coder-agent/coder").Return(nil,nil),
2125+
)
2126+
2127+
mClock.Set(time.Now()).MustWait(ctx)
2128+
tickerTrap:=mClock.Trap().TickerFunc("updaterLoop")
2129+
2130+
api:=agentcontainers.NewAPI(logger,
2131+
agentcontainers.WithClock(mClock),
2132+
agentcontainers.WithContainerCLI(mCCLI),
2133+
agentcontainers.WithDevcontainerCLI(fDCCLI),
2134+
agentcontainers.WithSubAgentClient(fSAC),
2135+
agentcontainers.WithSubAgentURL("test-subagent-url"),
2136+
agentcontainers.WithWatcher(watcher.NewNoop()),
2137+
agentcontainers.WithManifestInfo("test-user","test-workspace"),
2138+
)
2139+
api.Init()
2140+
deferapi.Close()
2141+
2142+
// Close before api.Close() defer to avoid deadlock after test.
2143+
deferclose(fSAC.createErrC)
2144+
deferclose(fDCCLI.readConfigErrC)
2145+
2146+
// Allow agent creation and injection to succeed.
2147+
testutil.RequireSend(ctx,t,fSAC.createErrC,nil)
2148+
2149+
testutil.RequireSend(ctx,t,fDCCLI.readConfigErrC,func(envs []string)error {
2150+
assert.Contains(t,envs,"CODER_WORKSPACE_AGENT_NAME=coder")
2151+
assert.Contains(t,envs,"CODER_WORKSPACE_NAME=test-workspace")
2152+
assert.Contains(t,envs,"CODER_WORKSPACE_OWNER_NAME=test-user")
2153+
assert.Contains(t,envs,"CODER_URL=test-subagent-url")
2154+
// First call should not have feature envs.
2155+
assert.NotContains(t,envs,"FEATURE_CODE_SERVER_OPTION_PORT=9090")
2156+
assert.NotContains(t,envs,"FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false")
2157+
returnnil
2158+
})
2159+
2160+
testutil.RequireSend(ctx,t,fDCCLI.readConfigErrC,func(envs []string)error {
2161+
assert.Contains(t,envs,"CODER_WORKSPACE_AGENT_NAME=coder")
2162+
assert.Contains(t,envs,"CODER_WORKSPACE_NAME=test-workspace")
2163+
assert.Contains(t,envs,"CODER_WORKSPACE_OWNER_NAME=test-user")
2164+
assert.Contains(t,envs,"CODER_URL=test-subagent-url")
2165+
// Second call should have feature envs from the first config read.
2166+
assert.Contains(t,envs,"FEATURE_CODE_SERVER_OPTION_PORT=9090")
2167+
assert.Contains(t,envs,"FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false")
2168+
returnnil
2169+
})
2170+
2171+
// Wait until the ticker has been registered.
2172+
tickerTrap.MustWait(ctx).MustRelease(ctx)
2173+
tickerTrap.Close()
2174+
2175+
// Verify agent was created successfully
2176+
require.Len(t,fSAC.created,1)
2177+
})
2178+
20632179
t.Run("CommandEnv",func(t*testing.T) {
20642180
t.Parallel()
20652181

‎agent/agentcontainers/devcontainercli.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66
"context"
77
"encoding/json"
88
"errors"
9+
"fmt"
910
"io"
11+
"slices"
12+
"strings"
1013

1114
"golang.org/x/xerrors"
1215

@@ -26,12 +29,55 @@ type DevcontainerConfig struct {
2629

2730
typeDevcontainerMergedConfigurationstruct {
2831
CustomizationsDevcontainerMergedCustomizations`json:"customizations,omitempty"`
32+
FeaturesDevcontainerFeatures`json:"features,omitempty"`
2933
}
3034

3135
typeDevcontainerMergedCustomizationsstruct {
3236
Coder []CoderCustomization`json:"coder,omitempty"`
3337
}
3438

39+
typeDevcontainerFeaturesmap[string]any
40+
41+
// OptionsAsEnvs converts the DevcontainerFeatures into a list of
42+
// environment variables that can be used to set feature options.
43+
// The format is FEATURE_<FEATURE_NAME>_OPTION_<OPTION_NAME>=<value>.
44+
// For example, if the feature is:
45+
//
46+
//"ghcr.io/coder/devcontainer-features/code-server:1": {
47+
// "port": 9090,
48+
// }
49+
//
50+
// It will produce:
51+
//
52+
//FEATURE_CODE_SERVER_OPTION_PORT=9090
53+
//
54+
// Note that the feature name is derived from the last part of the key,
55+
// so "ghcr.io/coder/devcontainer-features/code-server:1" becomes
56+
// "CODE_SERVER". The version part (e.g. ":1") is removed, and dashes in
57+
// the feature and option names are replaced with underscores.
58+
func (fDevcontainerFeatures)OptionsAsEnvs() []string {
59+
varenv []string
60+
fork,v:=rangef {
61+
vv,ok:=v.(map[string]any)
62+
if!ok {
63+
continue
64+
}
65+
// Take the last part of the key as the feature name/path.
66+
k=k[strings.LastIndex(k,"/")+1:]
67+
// Remove ":" and anything following it.
68+
ifidx:=strings.Index(k,":");idx!=-1 {
69+
k=k[:idx]
70+
}
71+
k=strings.ReplaceAll(k,"-","_")
72+
fork2,v2:=rangevv {
73+
k2=strings.ReplaceAll(k2,"-","_")
74+
env=append(env,fmt.Sprintf("FEATURE_%s_OPTION_%s=%s",strings.ToUpper(k),strings.ToUpper(k2),fmt.Sprintf("%v",v2)))
75+
}
76+
}
77+
slices.Sort(env)
78+
returnenv
79+
}
80+
3581
typeDevcontainerConfigurationstruct {
3682
CustomizationsDevcontainerCustomizations`json:"customizations,omitempty"`
3783
}

‎agent/agentcontainers/devcontainercli_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package agentcontainers_test
33
import (
44
"bytes"
55
"context"
6+
"encoding/json"
67
"errors"
78
"flag"
89
"fmt"
@@ -13,6 +14,7 @@ import (
1314
"strings"
1415
"testing"
1516

17+
"github.com/google/go-cmp/cmp"
1618
"github.com/ory/dockertest/v3"
1719
"github.com/ory/dockertest/v3/docker"
1820
"github.com/stretchr/testify/assert"
@@ -637,3 +639,107 @@ func removeDevcontainerByID(t *testing.T, pool *dockertest.Pool, id string) {
637639
assert.NoError(t,err,"remove container failed")
638640
}
639641
}
642+
643+
funcTestDevcontainerFeatures_OptionsAsEnvs(t*testing.T) {
644+
t.Parallel()
645+
646+
realConfigJSON:=`{
647+
"mergedConfiguration": {
648+
"features": {
649+
"./code-server": {
650+
"port": 9090
651+
},
652+
"ghcr.io/devcontainers/features/docker-in-docker:2": {
653+
"moby": "false"
654+
}
655+
}
656+
}
657+
}`
658+
varrealConfig agentcontainers.DevcontainerConfig
659+
err:=json.Unmarshal([]byte(realConfigJSON),&realConfig)
660+
require.NoError(t,err,"unmarshal JSON payload")
661+
662+
tests:= []struct {
663+
namestring
664+
features agentcontainers.DevcontainerFeatures
665+
want []string
666+
}{
667+
{
668+
name:"code-server feature",
669+
features: agentcontainers.DevcontainerFeatures{
670+
"./code-server":map[string]any{
671+
"port":9090,
672+
},
673+
},
674+
want: []string{
675+
"FEATURE_CODE_SERVER_OPTION_PORT=9090",
676+
},
677+
},
678+
{
679+
name:"docker-in-docker feature",
680+
features: agentcontainers.DevcontainerFeatures{
681+
"ghcr.io/devcontainers/features/docker-in-docker:2":map[string]any{
682+
"moby":"false",
683+
},
684+
},
685+
want: []string{
686+
"FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false",
687+
},
688+
},
689+
{
690+
name:"multiple features with multiple options",
691+
features: agentcontainers.DevcontainerFeatures{
692+
"./code-server":map[string]any{
693+
"port":9090,
694+
"password":"secret",
695+
},
696+
"ghcr.io/devcontainers/features/docker-in-docker:2":map[string]any{
697+
"moby":"false",
698+
"docker-dash-compose-version":"v2",
699+
},
700+
},
701+
want: []string{
702+
"FEATURE_CODE_SERVER_OPTION_PASSWORD=secret",
703+
"FEATURE_CODE_SERVER_OPTION_PORT=9090",
704+
"FEATURE_DOCKER_IN_DOCKER_OPTION_DOCKER_DASH_COMPOSE_VERSION=v2",
705+
"FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false",
706+
},
707+
},
708+
{
709+
name:"feature with non-map value (should be ignored)",
710+
features: agentcontainers.DevcontainerFeatures{
711+
"./code-server":map[string]any{
712+
"port":9090,
713+
},
714+
"./invalid-feature":"not-a-map",
715+
},
716+
want: []string{
717+
"FEATURE_CODE_SERVER_OPTION_PORT=9090",
718+
},
719+
},
720+
{
721+
name:"real config example",
722+
features:realConfig.MergedConfiguration.Features,
723+
want: []string{
724+
"FEATURE_CODE_SERVER_OPTION_PORT=9090",
725+
"FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false",
726+
},
727+
},
728+
{
729+
name:"empty features",
730+
features: agentcontainers.DevcontainerFeatures{},
731+
want:nil,
732+
},
733+
}
734+
735+
for_,tt:=rangetests {
736+
t.Run(tt.name,func(t*testing.T) {
737+
t.Parallel()
738+
739+
got:=tt.features.OptionsAsEnvs()
740+
ifdiff:=cmp.Diff(tt.want,got);diff!="" {
741+
require.Failf(t,"OptionsAsEnvs() mismatch (-want +got):\n%s",diff)
742+
}
743+
})
744+
}
745+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp