Skip to content

Commit 0ba200c

Browse files
authored
feat: Enable workspace debug logging (#6838)
* feat: Enable workspace debug logging * Fix * Fix * Fix * fix * fix * Enable RBAC * unit tests * Fix * fix * fix * fix * more tests * fix: workspacebuild_test use roles * fix: swagger comment * fix: ctx.Done * fix: address PR comments * break loop
1 parent 665b84d commit 0ba200c

File tree

18 files changed

+610
-288
lines changed

18 files changed

+610
-288
lines changed

coderd/apidoc/docs.go

+20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/provisionerdserver/provisionerdserver.go

+2
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
273273
TemplateName: template.Name,
274274
TemplateVersion: templateVersion.Name,
275275
},
276+
LogLevel: input.LogLevel,
276277
},
277278
}
278279
case database.ProvisionerJobTypeTemplateVersionDryRun:
@@ -1550,6 +1551,7 @@ type TemplateVersionImportJob struct {
15501551
type WorkspaceProvisionJob struct {
15511552
WorkspaceBuildID uuid.UUID `json:"workspace_build_id"`
15521553
DryRun bool `json:"dry_run"`
1554+
LogLevel string `json:"log_level,omitempty"`
15531555
}
15541556

15551557
// TemplateVersionDryRunJob is the payload for the "template_version_dry_run" job type.

coderd/workspacebuilds.go

+8
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,13 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
545545
}
546546
}
547547

548+
if createBuild.LogLevel != "" && !api.Authorize(r, rbac.ActionUpdate, template) {
549+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
550+
Message: "Workspace builds with a custom log level are restricted to template authors only.",
551+
})
552+
return
553+
}
554+
548555
var workspaceBuild database.WorkspaceBuild
549556
var provisionerJob database.ProvisionerJob
550557
// This must happen in a transaction to ensure history can be inserted, and
@@ -582,6 +589,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
582589
workspaceBuildID := uuid.New()
583590
input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{
584591
WorkspaceBuildID: workspaceBuildID,
592+
LogLevel: string(createBuild.LogLevel),
585593
})
586594
if err != nil {
587595
return xerrors.Errorf("marshal provision job: %w", err)

coderd/workspacebuilds_test.go

+132
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import (
1212
"github.com/google/uuid"
1313
"github.com/stretchr/testify/assert"
1414
"github.com/stretchr/testify/require"
15+
"golang.org/x/xerrors"
1516

1617
"github.com/coder/coder/coderd/audit"
1718
"github.com/coder/coder/coderd/coderdtest"
1819
"github.com/coder/coder/coderd/database"
20+
"github.com/coder/coder/coderd/rbac"
1921
"github.com/coder/coder/codersdk"
2022
"github.com/coder/coder/provisioner/echo"
2123
"github.com/coder/coder/provisionersdk/proto"
@@ -1151,3 +1153,133 @@ func TestMigrateLegacyToRichParameters(t *testing.T) {
11511153
require.Len(t, buildParameters, 1)
11521154
require.Equal(t, "carrot", buildParameters[0].Value)
11531155
}
1156+
1157+
func TestWorkspaceBuildDebugMode(t *testing.T) {
1158+
t.Parallel()
1159+
1160+
t.Run("AsRegularUser", func(t *testing.T) {
1161+
t.Parallel()
1162+
1163+
// Create users
1164+
templateAuthorClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
1165+
templateAuthor := coderdtest.CreateFirstUser(t, templateAuthorClient)
1166+
regularUserClient, _ := coderdtest.CreateAnotherUser(t, templateAuthorClient, templateAuthor.OrganizationID)
1167+
1168+
// Template owner: create a template
1169+
version := coderdtest.CreateTemplateVersion(t, templateAuthorClient, templateAuthor.OrganizationID, nil)
1170+
template := coderdtest.CreateTemplate(t, templateAuthorClient, templateAuthor.OrganizationID, version.ID)
1171+
coderdtest.AwaitTemplateVersionJob(t, templateAuthorClient, version.ID)
1172+
1173+
// Regular user: create a workspace
1174+
workspace := coderdtest.CreateWorkspace(t, regularUserClient, templateAuthor.OrganizationID, template.ID)
1175+
coderdtest.AwaitWorkspaceBuildJob(t, regularUserClient, workspace.LatestBuild.ID)
1176+
1177+
// Regular user: try to start a workspace build in debug mode
1178+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1179+
defer cancel()
1180+
1181+
_, err := regularUserClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
1182+
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
1183+
Transition: codersdk.WorkspaceTransitionStart,
1184+
LogLevel: "debug",
1185+
})
1186+
1187+
// Regular user: expect an error
1188+
require.NotNil(t, err)
1189+
var sdkError *codersdk.Error
1190+
isSdkError := xerrors.As(err, &sdkError)
1191+
require.True(t, isSdkError)
1192+
require.Contains(t, sdkError.Message, "Workspace builds with a custom log level are restricted to template authors only.")
1193+
})
1194+
t.Run("AsTemplateAuthor", func(t *testing.T) {
1195+
t.Parallel()
1196+
1197+
// Create users
1198+
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
1199+
admin := coderdtest.CreateFirstUser(t, adminClient)
1200+
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID, rbac.RoleTemplateAdmin())
1201+
1202+
// Interact as template admin
1203+
echoResponses := &echo.Responses{
1204+
Parse: echo.ParseComplete,
1205+
ProvisionPlan: echo.ProvisionComplete,
1206+
ProvisionApply: []*proto.Provision_Response{{
1207+
Type: &proto.Provision_Response_Log{
1208+
Log: &proto.Log{
1209+
Level: proto.LogLevel_DEBUG,
1210+
Output: "want-it",
1211+
},
1212+
},
1213+
}, {
1214+
Type: &proto.Provision_Response_Log{
1215+
Log: &proto.Log{
1216+
Level: proto.LogLevel_TRACE,
1217+
Output: "dont-want-it",
1218+
},
1219+
},
1220+
}, {
1221+
Type: &proto.Provision_Response_Log{
1222+
Log: &proto.Log{
1223+
Level: proto.LogLevel_DEBUG,
1224+
Output: "done",
1225+
},
1226+
},
1227+
}, {
1228+
Type: &proto.Provision_Response_Complete{
1229+
Complete: &proto.Provision_Complete{},
1230+
},
1231+
}},
1232+
}
1233+
version := coderdtest.CreateTemplateVersion(t, templateAdminClient, admin.OrganizationID, echoResponses)
1234+
template := coderdtest.CreateTemplate(t, templateAdminClient, admin.OrganizationID, version.ID)
1235+
coderdtest.AwaitTemplateVersionJob(t, templateAdminClient, version.ID)
1236+
1237+
// Create workspace
1238+
workspace := coderdtest.CreateWorkspace(t, templateAdminClient, admin.OrganizationID, template.ID)
1239+
coderdtest.AwaitWorkspaceBuildJob(t, templateAdminClient, workspace.LatestBuild.ID)
1240+
1241+
// Create workspace build
1242+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1243+
defer cancel()
1244+
1245+
build, err := templateAdminClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
1246+
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
1247+
Transition: codersdk.WorkspaceTransitionStart,
1248+
ProvisionerState: []byte(" "),
1249+
LogLevel: "debug",
1250+
})
1251+
require.Nil(t, err)
1252+
1253+
build = coderdtest.AwaitWorkspaceBuildJob(t, templateAdminClient, build.ID)
1254+
1255+
// Watch for incoming logs
1256+
logs, closer, err := templateAdminClient.WorkspaceBuildLogsAfter(ctx, build.ID, 0)
1257+
require.NoError(t, err)
1258+
defer closer.Close()
1259+
1260+
var logsProcessed int
1261+
1262+
processingLogs:
1263+
for {
1264+
select {
1265+
case <-ctx.Done():
1266+
require.Fail(t, "timeout occurred while processing logs")
1267+
return
1268+
case log, ok := <-logs:
1269+
if !ok {
1270+
break processingLogs
1271+
}
1272+
1273+
logsProcessed++
1274+
1275+
require.NotEqual(t, "dont-want-it", log.Output, "unexpected log message", "%s log message shouldn't be logged: %s")
1276+
1277+
if log.Output == "done" {
1278+
break processingLogs
1279+
}
1280+
}
1281+
}
1282+
1283+
require.Len(t, echoResponses.ProvisionApply, logsProcessed)
1284+
})
1285+
}

codersdk/workspaces.go

+9
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ type WorkspacesResponse struct {
4646
Count int `json:"count"`
4747
}
4848

49+
type ProvisionerLogLevel string
50+
51+
const (
52+
ProvisionerLogLevelDebug ProvisionerLogLevel = "debug"
53+
)
54+
4955
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
5056
type CreateWorkspaceBuildRequest struct {
5157
TemplateVersionID uuid.UUID `json:"template_version_id,omitempty" format:"uuid"`
@@ -59,6 +65,9 @@ type CreateWorkspaceBuildRequest struct {
5965
// This will not delete old params not included in this list.
6066
ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"`
6167
RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"`
68+
69+
// Log level changes the default logging verbosity of a provider ("info" if empty).
70+
LogLevel ProvisionerLogLevel `json:"log_level,omitempty" validate:"omitempty,oneof=debug"`
6271
}
6372

6473
type WorkspaceOptions struct {

docs/api/builds.md

+1
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
11661166
```json
11671167
{
11681168
"dry_run": true,
1169+
"log_level": "debug",
11691170
"orphan": true,
11701171
"parameter_values": [
11711172
{

docs/api/schemas.md

+17
Original file line numberDiff line numberDiff line change
@@ -1463,6 +1463,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
14631463
```json
14641464
{
14651465
"dry_run": true,
1466+
"log_level": "debug",
14661467
"orphan": true,
14671468
"parameter_values": [
14681469
{
@@ -1490,6 +1491,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
14901491
| Name | Type | Required | Restrictions | Description |
14911492
| ----------------------- | ----------------------------------------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
14921493
| `dry_run` | boolean | false | | |
1494+
| `log_level` | [codersdk.ProvisionerLogLevel](#codersdkprovisionerloglevel) | false | | Log level changes the default logging verbosity of a provider ("info" if empty). |
14931495
| `orphan` | boolean | false | | Orphan may be set for the Destroy transition. |
14941496
| `parameter_values` | array of [codersdk.CreateParameterRequest](#codersdkcreateparameterrequest) | false | | Parameter values are optional. It will write params to the 'workspace' scope. This will overwrite any existing parameters with the same name. This will not delete old params not included in this list. |
14951497
| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | |
@@ -1501,6 +1503,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
15011503

15021504
| Property | Value |
15031505
| ------------ | -------- |
1506+
| `log_level` | `debug` |
15041507
| `transition` | `create` |
15051508
| `transition` | `start` |
15061509
| `transition` | `stop` |
@@ -3259,6 +3262,20 @@ Parameter represents a set value for the scope.
32593262
| `canceled` |
32603263
| `failed` |
32613264

3265+
## codersdk.ProvisionerLogLevel
3266+
3267+
```json
3268+
"debug"
3269+
```
3270+
3271+
### Properties
3272+
3273+
#### Enumerated Values
3274+
3275+
| Value |
3276+
| ------- |
3277+
| `debug` |
3278+
32623279
## codersdk.ProvisionerStorageMethod
32633280

32643281
```json

provisioner/echo/serve.go

+26-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,12 @@ func (e *echo) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
192192
if err != nil {
193193
return xerrors.Errorf("unmarshal: %w", err)
194194
}
195-
err = stream.Send(&response)
195+
r, ok := filterLogResponses(config, &response)
196+
if !ok {
197+
continue
198+
}
199+
200+
err = stream.Send(r)
196201
if err != nil {
197202
return err
198203
}
@@ -282,3 +287,23 @@ func Tar(responses *Responses) ([]byte, error) {
282287
}
283288
return buffer.Bytes(), nil
284289
}
290+
291+
func filterLogResponses(config *proto.Provision_Config, response *proto.Provision_Response) (*proto.Provision_Response, bool) {
292+
responseLog, ok := response.Type.(*proto.Provision_Response_Log)
293+
if !ok {
294+
// Pass all non-log responses
295+
return response, true
296+
}
297+
298+
if config.ProvisionerLogLevel == "" {
299+
// Don't change the default behavior of "echo"
300+
return response, true
301+
}
302+
303+
provisionerLogLevel := proto.LogLevel_value[strings.ToUpper(config.ProvisionerLogLevel)]
304+
if int32(responseLog.Log.Level) < provisionerLogLevel {
305+
// Log level is not enabled
306+
return nil, false
307+
}
308+
return response, true
309+
}

0 commit comments

Comments
 (0)