Skip to content

Commit b2b6429

Browse files
committed
endpoint
1 parent 73d50c4 commit b2b6429

File tree

4 files changed

+231
-13
lines changed

4 files changed

+231
-13
lines changed

coderd/coderd.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,19 +1368,23 @@ func New(options *Options) *API {
13681368
r.Get("/timings", api.workspaceTimings)
13691369
})
13701370
})
1371-
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
1372-
r.Use(
1373-
apiKeyMiddleware,
1374-
httpmw.ExtractWorkspaceBuildParam(options.Database),
1375-
httpmw.ExtractWorkspaceParam(options.Database),
1376-
)
1377-
r.Get("/", api.workspaceBuild)
1378-
r.Patch("/cancel", api.patchCancelWorkspaceBuild)
1379-
r.Get("/logs", api.workspaceBuildLogs)
1380-
r.Get("/parameters", api.workspaceBuildParameters)
1381-
r.Get("/resources", api.workspaceBuildResourcesDeprecated)
1382-
r.Get("/state", api.workspaceBuildState)
1383-
r.Get("/timings", api.workspaceBuildTimings)
1371+
r.Route("/workspacebuilds", func(r chi.Router) {
1372+
r.Use(apiKeyMiddleware)
1373+
r.Get("/parameters", api.workspaceBuildParametersBulk)
1374+
1375+
r.Route("/{workspacebuild}", func(r chi.Router) {
1376+
r.Use(
1377+
httpmw.ExtractWorkspaceBuildParam(options.Database),
1378+
httpmw.ExtractWorkspaceParam(options.Database),
1379+
)
1380+
r.Get("/", api.workspaceBuild)
1381+
r.Patch("/cancel", api.patchCancelWorkspaceBuild)
1382+
r.Get("/logs", api.workspaceBuildLogs)
1383+
r.Get("/parameters", api.workspaceBuildParameters)
1384+
r.Get("/resources", api.workspaceBuildResourcesDeprecated)
1385+
r.Get("/state", api.workspaceBuildState)
1386+
r.Get("/timings", api.workspaceBuildTimings)
1387+
})
13841388
})
13851389
r.Route("/authcheck", func(r chi.Router) {
13861390
r.Use(apiKeyMiddleware)

coderd/workspacebuilds.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"slices"
1111
"sort"
1212
"strconv"
13+
"strings"
1314
"time"
1415

1516
"github.com/go-chi/chi/v5"
@@ -676,6 +677,65 @@ func (api *API) workspaceBuildParameters(rw http.ResponseWriter, r *http.Request
676677
httpapi.Write(ctx, rw, http.StatusOK, apiParameters)
677678
}
678679

680+
// @Summary Get build parameters for multiple workspace builds
681+
// @ID get-build-parameters-for-multiple-workspace-builds
682+
// @Security CoderSessionToken
683+
// @Produce json
684+
// @Tags Builds
685+
// @Param build_ids query string true "Comma-separated workspace build IDs"
686+
// @Success 200 {object} codersdk.WorkspaceBuildParametersBulkResponse
687+
// @Router /workspacebuilds/parameters [get]
688+
func (api *API) workspaceBuildParametersBulk(rw http.ResponseWriter, r *http.Request) {
689+
ctx := r.Context()
690+
691+
buildIDsParam := r.URL.Query().Get("build_ids")
692+
if buildIDsParam == "" {
693+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
694+
Message: "build_ids query parameter is required",
695+
})
696+
return
697+
}
698+
699+
// Parse build IDs
700+
buildIDStrings := strings.Split(buildIDsParam, ",")
701+
buildIDs := make([]uuid.UUID, 0, len(buildIDStrings))
702+
for _, idStr := range buildIDStrings {
703+
id, err := uuid.Parse(strings.TrimSpace(idStr))
704+
if err != nil {
705+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
706+
Message: "Invalid build ID format",
707+
Detail: err.Error(),
708+
})
709+
return
710+
}
711+
buildIDs = append(buildIDs, id)
712+
}
713+
714+
parameters, err := api.Database.GetWorkspaceBuildParametersByBuildIDs(ctx, buildIDs)
715+
if err != nil {
716+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
717+
Message: "Internal error fetching workspace build parameters.",
718+
Detail: err.Error(),
719+
})
720+
return
721+
}
722+
723+
// Group parameters by build ID
724+
parametersByBuildID := make(map[string][]codersdk.WorkspaceBuildParameter)
725+
for _, param := range parameters {
726+
buildID := param.WorkspaceBuildID.String()
727+
if parametersByBuildID[buildID] == nil {
728+
parametersByBuildID[buildID] = []codersdk.WorkspaceBuildParameter{}
729+
}
730+
parametersByBuildID[buildID] = append(parametersByBuildID[buildID], codersdk.WorkspaceBuildParameter{
731+
Name: param.Name,
732+
Value: param.Value,
733+
})
734+
}
735+
736+
httpapi.Write(ctx, rw, http.StatusOK, parametersByBuildID)
737+
}
738+
679739
// @Summary Get workspace build logs
680740
// @ID get-workspace-build-logs
681741
// @Security CoderSessionToken

coderd/workspacebuilds_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,3 +1783,129 @@ func TestWorkspaceBuildTimings(t *testing.T) {
17831783
require.Len(t, res.AgentConnectionTimings, 5)
17841784
})
17851785
}
1786+
1787+
func TestWorkspaceBuildParametersBulk(t *testing.T) {
1788+
t.Parallel()
1789+
1790+
t.Run("EmptyBuildIDs", func(t *testing.T) {
1791+
t.Parallel()
1792+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
1793+
_ = coderdtest.CreateFirstUser(t, client)
1794+
1795+
ctx := testutil.Context(t, testutil.WaitShort)
1796+
1797+
// Test with empty build IDs
1798+
params, err := client.WorkspaceBuildParametersBulk(ctx, []uuid.UUID{})
1799+
require.NoError(t, err)
1800+
require.Empty(t, params)
1801+
})
1802+
1803+
t.Run("MultipleBuilds", func(t *testing.T) {
1804+
t.Parallel()
1805+
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
1806+
first := coderdtest.CreateFirstUser(t, adminClient)
1807+
memberClient, _ := coderdtest.CreateAnotherUser(t, adminClient, first.OrganizationID)
1808+
1809+
ctx := testutil.Context(t, testutil.WaitLong)
1810+
1811+
// Create a template with parameters
1812+
version := coderdtest.CreateTemplateVersion(t, adminClient, first.OrganizationID, &echo.Responses{
1813+
Parse: echo.ParseComplete,
1814+
ProvisionPlan: []*proto.Response{{
1815+
Type: &proto.Response_Plan{
1816+
Plan: &proto.PlanComplete{
1817+
Parameters: []*proto.RichParameter{
1818+
{
1819+
Name: "param1",
1820+
Type: "string",
1821+
DefaultValue: "default1",
1822+
},
1823+
{
1824+
Name: "param2",
1825+
Type: "string",
1826+
DefaultValue: "default2",
1827+
},
1828+
},
1829+
},
1830+
},
1831+
}},
1832+
ProvisionApply: echo.ApplyComplete,
1833+
})
1834+
template := coderdtest.CreateTemplate(t, adminClient, first.OrganizationID, version.ID)
1835+
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, version.ID)
1836+
1837+
// Create two workspaces with different parameters
1838+
workspace1 := coderdtest.CreateWorkspace(t, memberClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
1839+
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
1840+
{Name: "param1", Value: "value1a"},
1841+
{Name: "param2", Value: "value2a"},
1842+
}
1843+
})
1844+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace1.LatestBuild.ID)
1845+
1846+
workspace2 := coderdtest.CreateWorkspace(t, memberClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
1847+
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
1848+
{Name: "param1", Value: "value1b"},
1849+
{Name: "param2", Value: "value2b"},
1850+
}
1851+
})
1852+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace2.LatestBuild.ID)
1853+
1854+
workspace3 := coderdtest.CreateWorkspace(t, adminClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
1855+
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
1856+
{Name: "param1", Value: "value1c"},
1857+
{Name: "param2", Value: "value2c"},
1858+
}
1859+
})
1860+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, adminClient, workspace3.LatestBuild.ID)
1861+
allBuildIDs := []uuid.UUID{workspace1.LatestBuild.ID, workspace2.LatestBuild.ID, workspace3.LatestBuild.ID}
1862+
1863+
// Test parameters endpoint as member
1864+
paramsByBuild, err := memberClient.WorkspaceBuildParametersBulk(ctx, allBuildIDs)
1865+
require.NoError(t, err)
1866+
require.Len(t, paramsByBuild, 2)
1867+
1868+
// Check workspace1 parameters
1869+
build1Params := paramsByBuild[workspace1.LatestBuild.ID.String()]
1870+
require.Len(t, build1Params, 2)
1871+
require.ElementsMatch(t, build1Params, []codersdk.WorkspaceBuildParameter{
1872+
{Name: "param1", Value: "value1a"},
1873+
{Name: "param2", Value: "value2a"},
1874+
})
1875+
1876+
// Check workspace2 parameters
1877+
build2Params := paramsByBuild[workspace2.LatestBuild.ID.String()]
1878+
require.Len(t, build2Params, 2)
1879+
require.ElementsMatch(t, build2Params, []codersdk.WorkspaceBuildParameter{
1880+
{Name: "param1", Value: "value1b"},
1881+
{Name: "param2", Value: "value2b"},
1882+
})
1883+
1884+
// Test parameters endpoint as admin
1885+
paramsByBuild, err = adminClient.WorkspaceBuildParametersBulk(ctx, allBuildIDs)
1886+
require.NoError(t, err)
1887+
require.Len(t, paramsByBuild, 3)
1888+
1889+
// Check workspace3 parameters
1890+
build3Params := paramsByBuild[workspace3.LatestBuild.ID.String()]
1891+
require.Len(t, build3Params, 2)
1892+
require.ElementsMatch(t, build3Params, []codersdk.WorkspaceBuildParameter{
1893+
{Name: "param1", Value: "value1c"},
1894+
{Name: "param2", Value: "value2c"},
1895+
})
1896+
})
1897+
1898+
t.Run("NonExistentBuildIDs", func(t *testing.T) {
1899+
t.Parallel()
1900+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
1901+
_ = coderdtest.CreateFirstUser(t, client)
1902+
1903+
ctx := testutil.Context(t, testutil.WaitShort)
1904+
1905+
// Test with non-existent build IDs
1906+
nonExistentID := uuid.New()
1907+
params, err := client.WorkspaceBuildParametersBulk(ctx, []uuid.UUID{nonExistentID})
1908+
require.NoError(t, err)
1909+
require.Empty(t, params)
1910+
})
1911+
}

codersdk/workspacebuilds.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"net/http"
9+
"strings"
910
"time"
1011

1112
"github.com/google/uuid"
@@ -178,6 +179,31 @@ func (c *Client) WorkspaceBuildParameters(ctx context.Context, build uuid.UUID)
178179
return params, json.NewDecoder(res.Body).Decode(&params)
179180
}
180181

182+
// WorkspaceBuildParametersBulk returns parameters for multiple workspace builds by their IDs.
183+
func (c *Client) WorkspaceBuildParametersBulk(ctx context.Context, buildIDs []uuid.UUID) (map[string][]WorkspaceBuildParameter, error) {
184+
if len(buildIDs) == 0 {
185+
return make(map[string][]WorkspaceBuildParameter), nil
186+
}
187+
188+
// Convert UUIDs to strings and join them
189+
buildIDStrings := make([]string, len(buildIDs))
190+
for i, id := range buildIDs {
191+
buildIDStrings[i] = id.String()
192+
}
193+
buildIDsParam := strings.Join(buildIDStrings, ",")
194+
195+
res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspacebuilds/parameters", nil, WithQueryParam("build_ids", buildIDsParam))
196+
if err != nil {
197+
return nil, err
198+
}
199+
defer res.Body.Close()
200+
if res.StatusCode != http.StatusOK {
201+
return nil, ReadBodyAsError(res)
202+
}
203+
var params map[string][]WorkspaceBuildParameter
204+
return params, json.NewDecoder(res.Body).Decode(&params)
205+
}
206+
181207
type TimingStage string
182208

183209
const (
@@ -244,3 +270,5 @@ func (c *Client) WorkspaceBuildTimings(ctx context.Context, build uuid.UUID) (Wo
244270
var timings WorkspaceBuildTimings
245271
return timings, json.NewDecoder(res.Body).Decode(&timings)
246272
}
273+
274+
type WorkspaceBuildParametersBulkResponse map[string][]WorkspaceBuildParameter

0 commit comments

Comments
 (0)