Skip to content

feat: Enable workspace debug logging #6838

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Mar 30, 2023
20 changes: 20 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions coderd/provisionerdserver/provisionerdserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
TemplateName: template.Name,
TemplateVersion: templateVersion.Name,
},
LogLevel: input.LogLevel,
},
}
case database.ProvisionerJobTypeTemplateVersionDryRun:
Expand Down Expand Up @@ -1550,6 +1551,7 @@ type TemplateVersionImportJob struct {
type WorkspaceProvisionJob struct {
WorkspaceBuildID uuid.UUID `json:"workspace_build_id"`
DryRun bool `json:"dry_run"`
LogLevel string `json:"log_level,omitempty"`
}

// TemplateVersionDryRunJob is the payload for the "template_version_dry_run" job type.
Expand Down
8 changes: 8 additions & 0 deletions coderd/workspacebuilds.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,13 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
}
}

if createBuild.LogLevel != "" && !api.Authorize(r, rbac.ActionUpdate, template) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Workspace builds with a custom log level are restricted to template authors only.",
})
return
}

var workspaceBuild database.WorkspaceBuild
var provisionerJob database.ProvisionerJob
// This must happen in a transaction to ensure history can be inserted, and
Expand Down Expand Up @@ -582,6 +589,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
workspaceBuildID := uuid.New()
input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{
WorkspaceBuildID: workspaceBuildID,
LogLevel: string(createBuild.LogLevel),
})
if err != nil {
return xerrors.Errorf("marshal provision job: %w", err)
Expand Down
132 changes: 132 additions & 0 deletions coderd/workspacebuilds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"

"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
Expand Down Expand Up @@ -1151,3 +1153,133 @@ func TestMigrateLegacyToRichParameters(t *testing.T) {
require.Len(t, buildParameters, 1)
require.Equal(t, "carrot", buildParameters[0].Value)
}

func TestWorkspaceBuildDebugMode(t *testing.T) {
t.Parallel()

t.Run("AsRegularUser", func(t *testing.T) {
t.Parallel()

// Create users
templateAuthorClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
templateAuthor := coderdtest.CreateFirstUser(t, templateAuthorClient)
regularUserClient, _ := coderdtest.CreateAnotherUser(t, templateAuthorClient, templateAuthor.OrganizationID)

// Template owner: create a template
version := coderdtest.CreateTemplateVersion(t, templateAuthorClient, templateAuthor.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, templateAuthorClient, templateAuthor.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, templateAuthorClient, version.ID)

// Regular user: create a workspace
workspace := coderdtest.CreateWorkspace(t, regularUserClient, templateAuthor.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, regularUserClient, workspace.LatestBuild.ID)

// Regular user: try to start a workspace build in debug mode
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

_, err := regularUserClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
Transition: codersdk.WorkspaceTransitionStart,
LogLevel: "debug",
})

// Regular user: expect an error
require.NotNil(t, err)
var sdkError *codersdk.Error
isSdkError := xerrors.As(err, &sdkError)
require.True(t, isSdkError)
require.Contains(t, sdkError.Message, "Workspace builds with a custom log level are restricted to template authors only.")
})
t.Run("AsTemplateAuthor", func(t *testing.T) {
t.Parallel()

// Create users
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
admin := coderdtest.CreateFirstUser(t, adminClient)
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID, rbac.RoleTemplateAdmin())

// Interact as template admin
echoResponses := &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.ProvisionComplete,
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_DEBUG,
Output: "want-it",
},
},
}, {
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_TRACE,
Output: "dont-want-it",
},
},
}, {
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_DEBUG,
Output: "done",
},
},
}, {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
}
version := coderdtest.CreateTemplateVersion(t, templateAdminClient, admin.OrganizationID, echoResponses)
template := coderdtest.CreateTemplate(t, templateAdminClient, admin.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, templateAdminClient, version.ID)

// Create workspace
workspace := coderdtest.CreateWorkspace(t, templateAdminClient, admin.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, templateAdminClient, workspace.LatestBuild.ID)

// Create workspace build
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

build, err := templateAdminClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
Transition: codersdk.WorkspaceTransitionStart,
ProvisionerState: []byte(" "),
LogLevel: "debug",
})
require.Nil(t, err)

build = coderdtest.AwaitWorkspaceBuildJob(t, templateAdminClient, build.ID)

// Watch for incoming logs
logs, closer, err := templateAdminClient.WorkspaceBuildLogsAfter(ctx, build.ID, 0)
require.NoError(t, err)
defer closer.Close()

var logsProcessed int

processingLogs:
for {
select {
case <-ctx.Done():
require.Fail(t, "timeout occurred while processing logs")
return
case log, ok := <-logs:
if !ok {
break processingLogs
}

logsProcessed++

require.NotEqual(t, "dont-want-it", log.Output, "unexpected log message", "%s log message shouldn't be logged: %s")

if log.Output == "done" {
break processingLogs
}
}
}

require.Len(t, echoResponses.ProvisionApply, logsProcessed)
})
}
9 changes: 9 additions & 0 deletions codersdk/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ type WorkspacesResponse struct {
Count int `json:"count"`
}

type ProvisionerLogLevel string

const (
ProvisionerLogLevelDebug ProvisionerLogLevel = "debug"
)

// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
type CreateWorkspaceBuildRequest struct {
TemplateVersionID uuid.UUID `json:"template_version_id,omitempty" format:"uuid"`
Expand All @@ -59,6 +65,9 @@ type CreateWorkspaceBuildRequest struct {
// This will not delete old params not included in this list.
ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"`
RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"`

// Log level changes the default logging verbosity of a provider ("info" if empty).
LogLevel ProvisionerLogLevel `json:"log_level,omitempty" validate:"omitempty,oneof=debug"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Add a comment so this gets documented in the API doc

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

}

type WorkspaceOptions struct {
Expand Down
1 change: 1 addition & 0 deletions docs/api/builds.md
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
```json
{
"dry_run": true,
"log_level": "debug",
"orphan": true,
"parameter_values": [
{
Expand Down
17 changes: 17 additions & 0 deletions docs/api/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -1463,6 +1463,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
```json
{
"dry_run": true,
"log_level": "debug",
"orphan": true,
"parameter_values": [
{
Expand Down Expand Up @@ -1490,6 +1491,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
| Name | Type | Required | Restrictions | Description |
| ----------------------- | ----------------------------------------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dry_run` | boolean | false | | |
| `log_level` | [codersdk.ProvisionerLogLevel](#codersdkprovisionerloglevel) | false | | Log level changes the default logging verbosity of a provider ("info" if empty). |
| `orphan` | boolean | false | | Orphan may be set for the Destroy transition. |
| `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. |
| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | |
Expand All @@ -1501,6 +1503,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a

| Property | Value |
| ------------ | -------- |
| `log_level` | `debug` |
| `transition` | `create` |
| `transition` | `start` |
| `transition` | `stop` |
Expand Down Expand Up @@ -3251,6 +3254,20 @@ Parameter represents a set value for the scope.
| `canceled` |
| `failed` |

## codersdk.ProvisionerLogLevel

```json
"debug"
```

### Properties

#### Enumerated Values

| Value |
| ------- |
| `debug` |

## codersdk.ProvisionerStorageMethod

```json
Expand Down
27 changes: 26 additions & 1 deletion provisioner/echo/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,12 @@ func (e *echo) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
if err != nil {
return xerrors.Errorf("unmarshal: %w", err)
}
err = stream.Send(&response)
r, ok := filterLogResponses(config, &response)
if !ok {
continue
}

err = stream.Send(r)
if err != nil {
return err
}
Expand Down Expand Up @@ -282,3 +287,23 @@ func Tar(responses *Responses) ([]byte, error) {
}
return buffer.Bytes(), nil
}

func filterLogResponses(config *proto.Provision_Config, response *proto.Provision_Response) (*proto.Provision_Response, bool) {
responseLog, ok := response.Type.(*proto.Provision_Response_Log)
if !ok {
// Pass all non-log responses
return response, true
}

if config.ProvisionerLogLevel == "" {
// Don't change the default behavior of "echo"
return response, true
}

provisionerLogLevel := proto.LogLevel_value[strings.ToUpper(config.ProvisionerLogLevel)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to avoid having to upper/lowercase this? Not blocking, just curious.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose that the only way to do this is to switch to log_level: DEBUG, so that it's upper-case everywhere. Not sure if it's worth it.

if int32(responseLog.Log.Level) < provisionerLogLevel {
// Log level is not enabled
return nil, false
}
return response, true
}
Loading