Skip to content

Commit a67f5ed

Browse files
authored
Merge branch 'main' into cj/ui-workspace-table-update-icon
2 parents b3402f6 + f44969b commit a67f5ed

File tree

121 files changed

+6442
-1739
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+6442
-1739
lines changed

CLAUDE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,4 @@ Read [cursor rules](.cursorrules).
103103

104104
The frontend is contained in the site folder.
105105

106-
For building Frontend refer to [this document](docs/contributing/frontend.md)
107106
For building Frontend refer to [this document](docs/about/contributing/frontend.md)

agent/agentcontainers/api.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,11 @@ func (api *API) updaterLoop() {
378378
// and anyone looking to interact with the API.
379379
api.logger.Debug(api.ctx, "performing initial containers update")
380380
if err := api.updateContainers(api.ctx); err != nil {
381-
api.logger.Error(api.ctx, "initial containers update failed", slog.Error(err))
381+
if errors.Is(err, context.Canceled) {
382+
api.logger.Warn(api.ctx, "initial containers update canceled", slog.Error(err))
383+
} else {
384+
api.logger.Error(api.ctx, "initial containers update failed", slog.Error(err))
385+
}
382386
} else {
383387
api.logger.Debug(api.ctx, "initial containers update complete")
384388
}
@@ -399,7 +403,11 @@ func (api *API) updaterLoop() {
399403
case api.updateTrigger <- done:
400404
err := <-done
401405
if err != nil {
402-
api.logger.Error(api.ctx, "updater loop ticker failed", slog.Error(err))
406+
if errors.Is(err, context.Canceled) {
407+
api.logger.Warn(api.ctx, "updater loop ticker canceled", slog.Error(err))
408+
} else {
409+
api.logger.Error(api.ctx, "updater loop ticker failed", slog.Error(err))
410+
}
403411
}
404412
default:
405413
api.logger.Debug(api.ctx, "updater loop ticker skipped, update in progress")

agent/agentcontainers/containers_dockercli.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,10 @@ func (dcli *dockerCLI) List(ctx context.Context) (codersdk.WorkspaceAgentListCon
311311
// container IDs and returns the parsed output.
312312
// The stderr output is also returned for logging purposes.
313313
func runDockerInspect(ctx context.Context, execer agentexec.Execer, ids ...string) (stdout, stderr []byte, err error) {
314+
if ctx.Err() != nil {
315+
// If the context is done, we don't want to run the command.
316+
return []byte{}, []byte{}, ctx.Err()
317+
}
314318
var stdoutBuf, stderrBuf bytes.Buffer
315319
cmd := execer.CommandContext(ctx, "docker", append([]string{"inspect"}, ids...)...)
316320
cmd.Stdout = &stdoutBuf
@@ -319,6 +323,12 @@ func runDockerInspect(ctx context.Context, execer agentexec.Execer, ids ...strin
319323
stdout = bytes.TrimSpace(stdoutBuf.Bytes())
320324
stderr = bytes.TrimSpace(stderrBuf.Bytes())
321325
if err != nil {
326+
if ctx.Err() != nil {
327+
// If the context was canceled while running the command,
328+
// return the context error instead of the command error,
329+
// which is likely to be "signal: killed".
330+
return stdout, stderr, ctx.Err()
331+
}
322332
if bytes.Contains(stderr, []byte("No such object:")) {
323333
// This can happen if a container is deleted between the time we check for its existence and the time we inspect it.
324334
return stdout, stderr, nil

cli/testdata/coder_list_--output_json.golden

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@
6868
"available": 0,
6969
"most_recently_seen": null
7070
},
71-
"template_version_preset_id": null
71+
"template_version_preset_id": null,
72+
"has_ai_task": false
7273
},
7374
"latest_app_status": null,
7475
"outdated": false,

coderd/aitasks.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package coderd
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/google/uuid"
9+
10+
"github.com/coder/coder/v2/coderd/httpapi"
11+
"github.com/coder/coder/v2/codersdk"
12+
)
13+
14+
// This endpoint is experimental and not guaranteed to be stable, so we're not
15+
// generating public-facing documentation for it.
16+
func (api *API) aiTasksPrompts(rw http.ResponseWriter, r *http.Request) {
17+
ctx := r.Context()
18+
19+
buildIDsParam := r.URL.Query().Get("build_ids")
20+
if buildIDsParam == "" {
21+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
22+
Message: "build_ids query parameter is required",
23+
})
24+
return
25+
}
26+
27+
// Parse build IDs
28+
buildIDStrings := strings.Split(buildIDsParam, ",")
29+
buildIDs := make([]uuid.UUID, 0, len(buildIDStrings))
30+
for _, idStr := range buildIDStrings {
31+
id, err := uuid.Parse(strings.TrimSpace(idStr))
32+
if err != nil {
33+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
34+
Message: fmt.Sprintf("Invalid build ID format: %s", idStr),
35+
Detail: err.Error(),
36+
})
37+
return
38+
}
39+
buildIDs = append(buildIDs, id)
40+
}
41+
42+
parameters, err := api.Database.GetWorkspaceBuildParametersByBuildIDs(ctx, buildIDs)
43+
if err != nil {
44+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
45+
Message: "Internal error fetching workspace build parameters.",
46+
Detail: err.Error(),
47+
})
48+
return
49+
}
50+
51+
promptsByBuildID := make(map[string]string, len(parameters))
52+
for _, param := range parameters {
53+
if param.Name != codersdk.AITaskPromptParameterName {
54+
continue
55+
}
56+
buildID := param.WorkspaceBuildID.String()
57+
promptsByBuildID[buildID] = param.Value
58+
}
59+
60+
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AITasksPromptsResponse{
61+
Prompts: promptsByBuildID,
62+
})
63+
}

coderd/aitasks_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package coderd_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/uuid"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/v2/coderd/coderdtest"
10+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/coder/v2/provisioner/echo"
13+
"github.com/coder/coder/v2/provisionersdk/proto"
14+
"github.com/coder/coder/v2/testutil"
15+
)
16+
17+
func TestAITasksPrompts(t *testing.T) {
18+
t.Parallel()
19+
20+
t.Run("EmptyBuildIDs", func(t *testing.T) {
21+
t.Parallel()
22+
client := coderdtest.New(t, &coderdtest.Options{})
23+
_ = coderdtest.CreateFirstUser(t, client)
24+
experimentalClient := codersdk.NewExperimentalClient(client)
25+
26+
ctx := testutil.Context(t, testutil.WaitShort)
27+
28+
// Test with empty build IDs
29+
prompts, err := experimentalClient.AITaskPrompts(ctx, []uuid.UUID{})
30+
require.NoError(t, err)
31+
require.Empty(t, prompts.Prompts)
32+
})
33+
34+
t.Run("MultipleBuilds", func(t *testing.T) {
35+
t.Parallel()
36+
37+
if !dbtestutil.WillUsePostgres() {
38+
t.Skip("This test checks RBAC, which is not supported in the in-memory database")
39+
}
40+
41+
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
42+
first := coderdtest.CreateFirstUser(t, adminClient)
43+
memberClient, _ := coderdtest.CreateAnotherUser(t, adminClient, first.OrganizationID)
44+
45+
ctx := testutil.Context(t, testutil.WaitLong)
46+
47+
// Create a template with parameters
48+
version := coderdtest.CreateTemplateVersion(t, adminClient, first.OrganizationID, &echo.Responses{
49+
Parse: echo.ParseComplete,
50+
ProvisionPlan: []*proto.Response{{
51+
Type: &proto.Response_Plan{
52+
Plan: &proto.PlanComplete{
53+
Parameters: []*proto.RichParameter{
54+
{
55+
Name: "param1",
56+
Type: "string",
57+
DefaultValue: "default1",
58+
},
59+
{
60+
Name: codersdk.AITaskPromptParameterName,
61+
Type: "string",
62+
DefaultValue: "default2",
63+
},
64+
},
65+
},
66+
},
67+
}},
68+
ProvisionApply: echo.ApplyComplete,
69+
})
70+
template := coderdtest.CreateTemplate(t, adminClient, first.OrganizationID, version.ID)
71+
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, version.ID)
72+
73+
// Create two workspaces with different parameters
74+
workspace1 := coderdtest.CreateWorkspace(t, memberClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
75+
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
76+
{Name: "param1", Value: "value1a"},
77+
{Name: codersdk.AITaskPromptParameterName, Value: "value2a"},
78+
}
79+
})
80+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace1.LatestBuild.ID)
81+
82+
workspace2 := coderdtest.CreateWorkspace(t, memberClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
83+
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
84+
{Name: "param1", Value: "value1b"},
85+
{Name: codersdk.AITaskPromptParameterName, Value: "value2b"},
86+
}
87+
})
88+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace2.LatestBuild.ID)
89+
90+
workspace3 := coderdtest.CreateWorkspace(t, adminClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
91+
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
92+
{Name: "param1", Value: "value1c"},
93+
{Name: codersdk.AITaskPromptParameterName, Value: "value2c"},
94+
}
95+
})
96+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, adminClient, workspace3.LatestBuild.ID)
97+
allBuildIDs := []uuid.UUID{workspace1.LatestBuild.ID, workspace2.LatestBuild.ID, workspace3.LatestBuild.ID}
98+
99+
experimentalMemberClient := codersdk.NewExperimentalClient(memberClient)
100+
// Test parameters endpoint as member
101+
prompts, err := experimentalMemberClient.AITaskPrompts(ctx, allBuildIDs)
102+
require.NoError(t, err)
103+
// we expect 2 prompts because the member client does not have access to workspace3
104+
// since it was created by the admin client
105+
require.Len(t, prompts.Prompts, 2)
106+
107+
// Check workspace1 parameters
108+
build1Prompt := prompts.Prompts[workspace1.LatestBuild.ID.String()]
109+
require.Equal(t, "value2a", build1Prompt)
110+
111+
// Check workspace2 parameters
112+
build2Prompt := prompts.Prompts[workspace2.LatestBuild.ID.String()]
113+
require.Equal(t, "value2b", build2Prompt)
114+
115+
experimentalAdminClient := codersdk.NewExperimentalClient(adminClient)
116+
// Test parameters endpoint as admin
117+
// we expect 3 prompts because the admin client has access to all workspaces
118+
prompts, err = experimentalAdminClient.AITaskPrompts(ctx, allBuildIDs)
119+
require.NoError(t, err)
120+
require.Len(t, prompts.Prompts, 3)
121+
122+
// Check workspace3 parameters
123+
build3Prompt := prompts.Prompts[workspace3.LatestBuild.ID.String()]
124+
require.Equal(t, "value2c", build3Prompt)
125+
})
126+
127+
t.Run("NonExistentBuildIDs", func(t *testing.T) {
128+
t.Parallel()
129+
client := coderdtest.New(t, &coderdtest.Options{})
130+
_ = coderdtest.CreateFirstUser(t, client)
131+
132+
ctx := testutil.Context(t, testutil.WaitShort)
133+
134+
// Test with non-existent build IDs
135+
nonExistentID := uuid.New()
136+
experimentalClient := codersdk.NewExperimentalClient(client)
137+
prompts, err := experimentalClient.AITaskPrompts(ctx, []uuid.UUID{nonExistentID})
138+
require.NoError(t, err)
139+
require.Empty(t, prompts.Prompts)
140+
})
141+
}

coderd/apidoc/docs.go

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/autobuild/lifecycle_executor.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"database/sql"
66
"fmt"
77
"net/http"
8+
"slices"
9+
"strings"
810
"sync"
911
"sync/atomic"
1012
"time"
@@ -155,6 +157,22 @@ func (e *Executor) runOnce(t time.Time) Stats {
155157
return stats
156158
}
157159

160+
// Sort the workspaces by build template version ID so that we can group
161+
// identical template versions together. This is a slight (and imperfect)
162+
// optimization.
163+
//
164+
// `wsbuilder` needs to load the terraform files for a given template version
165+
// into memory. If 2 workspaces are using the same template version, they will
166+
// share the same files in the FileCache. This only happens if the builds happen
167+
// in parallel.
168+
// TODO: Actually make sure the cache has the files in the cache for the full
169+
// set of identical template versions. Then unload the files when the builds
170+
// are done. Right now, this relies on luck for the 10 goroutine workers to
171+
// overlap and keep the file reference in the cache alive.
172+
slices.SortFunc(workspaces, func(a, b database.GetWorkspacesEligibleForTransitionRow) int {
173+
return strings.Compare(a.BuildTemplateVersionID.UUID.String(), b.BuildTemplateVersionID.UUID.String())
174+
})
175+
158176
// We only use errgroup here for convenience of API, not for early
159177
// cancellation. This means we only return nil errors in th eg.Go.
160178
eg := errgroup.Group{}

coderd/coderd.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,14 @@ func New(options *Options) *API {
939939
})
940940
})
941941

942+
// Experimental routes are not guaranteed to be stable and may change at any time.
943+
r.Route("/api/experimental", func(r chi.Router) {
944+
r.Use(apiKeyMiddleware)
945+
r.Route("/aitasks", func(r chi.Router) {
946+
r.Get("/prompts", api.aiTasksPrompts)
947+
})
948+
})
949+
942950
r.Route("/api/v2", func(r chi.Router) {
943951
api.APIHandler = r
944952

@@ -1536,7 +1544,6 @@ func New(options *Options) *API {
15361544
// Add CSP headers to all static assets and pages. CSP headers only affect
15371545
// browsers, so these don't make sense on api routes.
15381546
cspMW := httpmw.CSPHeaders(
1539-
api.Experiments,
15401547
options.Telemetry.Enabled(), func() []*proxyhealth.ProxyHost {
15411548
if api.DeploymentValues.Dangerous.AllowAllCors {
15421549
// In this mode, allow all external requests.

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