Skip to content

Commit c3620e2

Browse files
chore: add tests and fix bug
1 parent f528eb3 commit c3620e2

File tree

4 files changed

+268
-3
lines changed

4 files changed

+268
-3
lines changed

agent/agentcontainers/api.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,12 +444,25 @@ func (api *API) discoverDevcontainerProjects() error {
444444
}
445445

446446
func (api *API) discoverDevcontainersInProject(projectPath string) error {
447+
devcontainerConfigPaths := []string{
448+
"/.devcontainer/devcontainer.json",
449+
"/.devcontainer.json",
450+
}
451+
447452
return afero.Walk(api.fs, projectPath, func(path string, info fs.FileInfo, err error) error {
448-
if strings.HasSuffix(path, ".devcontainer/devcontainer.json") {
449-
workspaceFolder := strings.TrimSuffix(path, ".devcontainer/devcontainer.json")
453+
for _, relativeConfigPath := range devcontainerConfigPaths {
454+
if !strings.HasSuffix(path, relativeConfigPath) {
455+
continue
456+
}
457+
458+
workspaceFolder := strings.TrimSuffix(path, relativeConfigPath)
459+
460+
api.logger.Debug(api.ctx, "discovered dev container project", slog.F("workspace_folder", workspaceFolder))
450461

451462
api.mu.Lock()
452463
if _, found := api.knownDevcontainers[workspaceFolder]; !found {
464+
api.logger.Debug(api.ctx, "adding dev container project", slog.F("workspace_folder", workspaceFolder))
465+
453466
dc := codersdk.WorkspaceAgentDevcontainer{
454467
ID: uuid.New(),
455468
Name: "", // Updated later based on container state.

agent/agentcontainers/api_test.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/go-chi/chi/v5"
2121
"github.com/google/uuid"
2222
"github.com/lib/pq"
23+
"github.com/spf13/afero"
2324
"github.com/stretchr/testify/assert"
2425
"github.com/stretchr/testify/require"
2526
"go.uber.org/mock/gomock"
@@ -3189,3 +3190,234 @@ func TestWithDevcontainersNameGeneration(t *testing.T) {
31893190
assert.Equal(t, "bar-project", response.Devcontainers[0].Name, "second devcontainer should has a collision and uses the folder name with a prefix")
31903191
assert.Equal(t, "baz-project", response.Devcontainers[1].Name, "third devcontainer should use the folder name with a prefix since it collides with the first two")
31913192
}
3193+
3194+
func TestDevcontainerDiscovery(t *testing.T) {
3195+
t.Parallel()
3196+
3197+
// We discover dev container projects by searching
3198+
// for git repositories at the agent's directory,
3199+
// and then recursively walking through these git
3200+
// repositories to find any `.devcontainer/devcontainer.json`
3201+
// files. These tests are to validate that behavior.
3202+
3203+
tests := []struct {
3204+
name string
3205+
agentDir string
3206+
fs map[string]string
3207+
expected []codersdk.WorkspaceAgentDevcontainer
3208+
}{
3209+
{
3210+
name: "GitProjectInRootDir/SingleProject",
3211+
agentDir: "/home/coder",
3212+
fs: map[string]string{
3213+
"/home/coder/.git/HEAD": "",
3214+
"/home/coder/.devcontainer/devcontainer.json": "",
3215+
},
3216+
expected: []codersdk.WorkspaceAgentDevcontainer{
3217+
{
3218+
WorkspaceFolder: "/home/coder",
3219+
ConfigPath: "/home/coder/.devcontainer/devcontainer.json",
3220+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3221+
},
3222+
},
3223+
},
3224+
{
3225+
name: "GitProjectInRootDir/MultipleProjects",
3226+
agentDir: "/home/coder",
3227+
fs: map[string]string{
3228+
"/home/coder/.git/HEAD": "",
3229+
"/home/coder/.devcontainer/devcontainer.json": "",
3230+
"/home/coder/site/.devcontainer/devcontainer.json": "",
3231+
},
3232+
expected: []codersdk.WorkspaceAgentDevcontainer{
3233+
{
3234+
WorkspaceFolder: "/home/coder",
3235+
ConfigPath: "/home/coder/.devcontainer/devcontainer.json",
3236+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3237+
},
3238+
{
3239+
WorkspaceFolder: "/home/coder/site",
3240+
ConfigPath: "/home/coder/site/.devcontainer/devcontainer.json",
3241+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3242+
},
3243+
},
3244+
},
3245+
{
3246+
name: "GitProjectInChildDir/SingleProject",
3247+
agentDir: "/home/coder",
3248+
fs: map[string]string{
3249+
"/home/coder/coder/.git/HEAD": "",
3250+
"/home/coder/coder/.devcontainer/devcontainer.json": "",
3251+
},
3252+
expected: []codersdk.WorkspaceAgentDevcontainer{
3253+
{
3254+
WorkspaceFolder: "/home/coder/coder",
3255+
ConfigPath: "/home/coder/coder/.devcontainer/devcontainer.json",
3256+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3257+
},
3258+
},
3259+
},
3260+
{
3261+
name: "GitProjectInChildDir/MultipleProjects",
3262+
agentDir: "/home/coder",
3263+
fs: map[string]string{
3264+
"/home/coder/coder/.git/HEAD": "",
3265+
"/home/coder/coder/.devcontainer/devcontainer.json": "",
3266+
"/home/coder/coder/site/.devcontainer/devcontainer.json": "",
3267+
},
3268+
expected: []codersdk.WorkspaceAgentDevcontainer{
3269+
{
3270+
WorkspaceFolder: "/home/coder/coder",
3271+
ConfigPath: "/home/coder/coder/.devcontainer/devcontainer.json",
3272+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3273+
},
3274+
{
3275+
WorkspaceFolder: "/home/coder/coder/site",
3276+
ConfigPath: "/home/coder/coder/site/.devcontainer/devcontainer.json",
3277+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3278+
},
3279+
},
3280+
},
3281+
{
3282+
name: "GitProjectInMultipleChildDirs/SingleProjectEach",
3283+
agentDir: "/home/coder",
3284+
fs: map[string]string{
3285+
"/home/coder/coder/.git/HEAD": "",
3286+
"/home/coder/coder/.devcontainer/devcontainer.json": "",
3287+
"/home/coder/envbuilder/.git/HEAD": "",
3288+
"/home/coder/envbuilder/.devcontainer/devcontainer.json": "",
3289+
},
3290+
expected: []codersdk.WorkspaceAgentDevcontainer{
3291+
{
3292+
WorkspaceFolder: "/home/coder/coder",
3293+
ConfigPath: "/home/coder/coder/.devcontainer/devcontainer.json",
3294+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3295+
},
3296+
{
3297+
WorkspaceFolder: "/home/coder/envbuilder",
3298+
ConfigPath: "/home/coder/envbuilder/.devcontainer/devcontainer.json",
3299+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3300+
},
3301+
},
3302+
},
3303+
{
3304+
name: "GitProjectInMultipleChildDirs/MultipleProjectEach",
3305+
agentDir: "/home/coder",
3306+
fs: map[string]string{
3307+
"/home/coder/coder/.git/HEAD": "",
3308+
"/home/coder/coder/.devcontainer/devcontainer.json": "",
3309+
"/home/coder/coder/site/.devcontainer/devcontainer.json": "",
3310+
"/home/coder/envbuilder/.git/HEAD": "",
3311+
"/home/coder/envbuilder/.devcontainer/devcontainer.json": "",
3312+
"/home/coder/envbuilder/x/.devcontainer/devcontainer.json": "",
3313+
},
3314+
expected: []codersdk.WorkspaceAgentDevcontainer{
3315+
{
3316+
WorkspaceFolder: "/home/coder/coder",
3317+
ConfigPath: "/home/coder/coder/.devcontainer/devcontainer.json",
3318+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3319+
},
3320+
{
3321+
WorkspaceFolder: "/home/coder/coder/site",
3322+
ConfigPath: "/home/coder/coder/site/.devcontainer/devcontainer.json",
3323+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3324+
},
3325+
{
3326+
WorkspaceFolder: "/home/coder/envbuilder",
3327+
ConfigPath: "/home/coder/envbuilder/.devcontainer/devcontainer.json",
3328+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3329+
},
3330+
{
3331+
WorkspaceFolder: "/home/coder/envbuilder/x",
3332+
ConfigPath: "/home/coder/envbuilder/x/.devcontainer/devcontainer.json",
3333+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3334+
},
3335+
},
3336+
},
3337+
}
3338+
3339+
initFS := func(t *testing.T, files map[string]string) afero.Fs {
3340+
t.Helper()
3341+
3342+
fs := afero.NewMemMapFs()
3343+
for name, content := range files {
3344+
err := afero.WriteFile(fs, name, []byte(content+"\n"), 0o600)
3345+
require.NoError(t, err)
3346+
}
3347+
return fs
3348+
}
3349+
3350+
for _, tt := range tests {
3351+
t.Run(tt.name, func(t *testing.T) {
3352+
t.Parallel()
3353+
3354+
var (
3355+
ctx = testutil.Context(t, testutil.WaitShort)
3356+
logger = testutil.Logger(t)
3357+
mClock = quartz.NewMock(t)
3358+
tickerTrap = mClock.Trap().TickerFunc("updaterLoop")
3359+
3360+
r = chi.NewRouter()
3361+
)
3362+
3363+
api := agentcontainers.NewAPI(logger,
3364+
agentcontainers.WithClock(mClock),
3365+
agentcontainers.WithWatcher(watcher.NewNoop()),
3366+
agentcontainers.WithFileSystem(initFS(t, tt.fs)),
3367+
agentcontainers.WithManifestInfo("owner", "workspace", "parent-agent", tt.agentDir),
3368+
agentcontainers.WithContainerCLI(&fakeContainerCLI{}),
3369+
agentcontainers.WithDevcontainerCLI(&fakeDevcontainerCLI{}),
3370+
)
3371+
api.Start()
3372+
defer api.Close()
3373+
r.Mount("/", api.Routes())
3374+
3375+
tickerTrap.MustWait(ctx).MustRelease(ctx)
3376+
tickerTrap.Close()
3377+
3378+
// Wait until all projects have been discovered
3379+
require.Eventuallyf(t, func() bool {
3380+
req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
3381+
rec := httptest.NewRecorder()
3382+
r.ServeHTTP(rec, req)
3383+
3384+
got := codersdk.WorkspaceAgentListContainersResponse{}
3385+
err := json.NewDecoder(rec.Body).Decode(&got)
3386+
require.NoError(t, err)
3387+
3388+
return len(got.Devcontainers) == len(tt.expected)
3389+
}, testutil.WaitShort, testutil.IntervalFast, "dev containers never found")
3390+
3391+
// Now projects have been discovered, we'll allow the updater loop
3392+
// to set the appropriate status for these containers.
3393+
_, aw := mClock.AdvanceNext()
3394+
aw.MustWait(ctx)
3395+
3396+
// Now we'll fetch the list of dev containers
3397+
req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
3398+
rec := httptest.NewRecorder()
3399+
r.ServeHTTP(rec, req)
3400+
3401+
got := codersdk.WorkspaceAgentListContainersResponse{}
3402+
err := json.NewDecoder(rec.Body).Decode(&got)
3403+
require.NoError(t, err)
3404+
3405+
// We will set the IDs of each dev container to uuid.Nil to simplify
3406+
// this check.
3407+
for idx := range got.Devcontainers {
3408+
got.Devcontainers[idx].ID = uuid.Nil
3409+
}
3410+
3411+
// Sort the expected dev containers and got dev containers by their workspace folder.
3412+
// This helps ensure a deterministic test.
3413+
slices.SortFunc(tt.expected, func(a, b codersdk.WorkspaceAgentDevcontainer) int {
3414+
return strings.Compare(a.WorkspaceFolder, b.WorkspaceFolder)
3415+
})
3416+
slices.SortFunc(got.Devcontainers, func(a, b codersdk.WorkspaceAgentDevcontainer) int {
3417+
return strings.Compare(a.WorkspaceFolder, b.WorkspaceFolder)
3418+
})
3419+
3420+
require.Equal(t, tt.expected, got.Devcontainers)
3421+
})
3422+
}
3423+
}

site/src/modules/resources/AgentDevcontainerCard.stories.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ export const Recreating: Story = {
9191
},
9292
};
9393

94+
export const NoContainerOrSubAgent: Story = {
95+
args: {
96+
devcontainer: {
97+
...MockWorkspaceAgentDevcontainer,
98+
container: undefined,
99+
agent: undefined,
100+
},
101+
subAgents: [],
102+
},
103+
};
104+
94105
export const NoSubAgent: Story = {
95106
args: {
96107
devcontainer: {

site/src/modules/resources/AgentRow.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,16 @@ export const AgentRow: FC<AgentRowProps> = ({
137137
const [showParentApps, setShowParentApps] = useState(false);
138138

139139
let shouldDisplayAppsSection = shouldDisplayAgentApps;
140-
if (devcontainers && devcontainers.length > 0 && !showParentApps) {
140+
if (
141+
devcontainers &&
142+
devcontainers.find(
143+
// We only want to hide the parent apps by default when there are dev
144+
// containers that are either starting or running. If they are all in
145+
// the stopped state, it doesn't make sense to hide the parent apps.
146+
(dc) => dc.status === "running" || dc.status === "starting",
147+
) !== undefined &&
148+
!showParentApps
149+
) {
141150
shouldDisplayAppsSection = false;
142151
}
143152

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy