Skip to content

feat: enable Terraform debug mode via deployment configuration #8260

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 12 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ Use a YAML configuration file when your server launch become unwieldy.
Write out the current server config as YAML to stdout.

Introspection / Logging Options
--enable-terraform-debug-mode bool, $CODER_ENABLE_TERRAFORM_DEBUG_MODE (default: false)
Allow administrators to enable Terraform debug output.

--log-human string, $CODER_LOGGING_HUMAN (default: /dev/stderr)
Output human-readable logs to a given file.

Expand Down
3 changes: 3 additions & 0 deletions cli/testdata/server-config.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ introspection:
# Output Stackdriver compatible logs to a given file.
# (default: <unset>, type: string)
stackdriverPath: ""
# Allow administrators to enable Terraform debug output.
# (default: false, type: bool)
enableTerraformDebugMode: false
oauth2:
github:
# Client ID for Login with GitHub.
Expand Down
3 changes: 3 additions & 0 deletions coderd/apidoc/docs.go

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

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

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

3 changes: 2 additions & 1 deletion coderd/workspacebuilds.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
builder := wsbuilder.New(workspace, database.WorkspaceTransition(createBuild.Transition)).
Initiator(apiKey.UserID).
RichParameterValues(createBuild.RichParameterValues).
LogLevel(string(createBuild.LogLevel))
LogLevel(string(createBuild.LogLevel)).
DeploymentValues(api.Options.DeploymentValues)

if createBuild.TemplateVersionID != uuid.Nil {
builder = builder.VersionID(createBuild.TemplateVersionID)
Expand Down
102 changes: 90 additions & 12 deletions coderd/workspacebuilds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -640,11 +640,50 @@ func TestWorkspaceBuildStatus(t *testing.T) {
func TestWorkspaceBuildDebugMode(t *testing.T) {
t.Parallel()

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

// Create user
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.EnableTerraformDebugMode = false

templateAuthorClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: deploymentValues})
templateAuthor := coderdtest.CreateFirstUser(t, templateAuthorClient)

// Template author: 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)

// Template author: create a workspace
workspace := coderdtest.CreateWorkspace(t, templateAuthorClient, templateAuthor.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, templateAuthorClient, workspace.LatestBuild.ID)

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

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

// Template author: expect an error as the debug mode is disabled
require.NotNil(t, err)
var sdkError *codersdk.Error
isSdkError := xerrors.As(err, &sdkError)
require.True(t, isSdkError)
require.Contains(t, sdkError.Message, "Terraform debug mode is disabled in the deployment configuration.")
})
t.Run("AsRegularUser", func(t *testing.T) {
t.Parallel()

// Create users
templateAuthorClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.EnableTerraformDebugMode = true

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

Expand Down Expand Up @@ -672,15 +711,54 @@ func TestWorkspaceBuildDebugMode(t *testing.T) {
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.")
require.Contains(t, sdkError.Message, "Workspace builds with a custom log level are restricted to administrators only.")
})
t.Run("AsTemplateAuthor", func(t *testing.T) {
t.Parallel()

// Create users
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.EnableTerraformDebugMode = true

adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: deploymentValues})
admin := coderdtest.CreateFirstUser(t, adminClient)
templateAuthorClient, _ := coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID, rbac.RoleTemplateAdmin())

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

// Template author: create a workspace
workspace := coderdtest.CreateWorkspace(t, templateAuthorClient, admin.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, templateAuthorClient, workspace.LatestBuild.ID)

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

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

// Template author: expect an error as the debug mode is disabled
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 administrators only.")
})
t.Run("AsAdmin", func(t *testing.T) {
t.Parallel()

// Create users
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.EnableTerraformDebugMode = true

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

// Interact as template admin
echoResponses := &echo.Responses{
Expand Down Expand Up @@ -713,30 +791,30 @@ func TestWorkspaceBuildDebugMode(t *testing.T) {
},
}},
}
version := coderdtest.CreateTemplateVersion(t, templateAdminClient, admin.OrganizationID, echoResponses)
template := coderdtest.CreateTemplate(t, templateAdminClient, admin.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, templateAdminClient, version.ID)
version := coderdtest.CreateTemplateVersion(t, adminClient, admin.OrganizationID, echoResponses)
template := coderdtest.CreateTemplate(t, adminClient, admin.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, adminClient, version.ID)

// Create workspace
workspace := coderdtest.CreateWorkspace(t, templateAdminClient, admin.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, templateAdminClient, workspace.LatestBuild.ID)
workspace := coderdtest.CreateWorkspace(t, adminClient, admin.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, adminClient, 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{
build, err := adminClient.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)
build = coderdtest.AwaitWorkspaceBuildJob(t, adminClient, build.ID)

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

Expand Down
32 changes: 24 additions & 8 deletions coderd/wsbuilder/wsbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ import (
// build, job, err := b.Build(...)
type Builder struct {
// settings that control the kind of build you get
workspace database.Workspace
trans database.WorkspaceTransition
version versionTarget
state stateTarget
logLevel string
workspace database.Workspace
trans database.WorkspaceTransition
version versionTarget
state stateTarget
logLevel string
deploymentValues *codersdk.DeploymentValues

richParameterValues []codersdk.WorkspaceBuildParameter
initiator uuid.UUID
reason database.BuildReason
Expand Down Expand Up @@ -128,6 +130,12 @@ func (b Builder) LogLevel(l string) Builder {
return b
}

func (b Builder) DeploymentValues(dv *codersdk.DeploymentValues) Builder {
// nolint: revive
b.deploymentValues = dv
return b
}

func (b Builder) Initiator(u uuid.UUID) Builder {
// nolint: revive
b.initiator = u
Expand Down Expand Up @@ -638,11 +646,19 @@ func (b *Builder) authorize(authFunc func(action rbac.Action, object rbac.Object
}
}

if b.logLevel != "" && !authFunc(rbac.ActionUpdate, template) {
if b.logLevel != "" && !authFunc(rbac.ActionRead, rbac.ResourceDeploymentValues) {
Copy link
Member

Choose a reason for hiding this comment

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

Is this the de-facto way to check for admin? It feels like it'd be one of those things that might change in the future (esp. a read permission). Probably fine for now though.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that's why I covered it on the workspace build level with tests for all crucial roles.

return BuildError{
http.StatusBadRequest,
"Workspace builds with a custom log level are restricted to administrators only.",
xerrors.New("Workspace builds with a custom log level are restricted to administrators only."),
}
}

if b.logLevel != "" && b.deploymentValues != nil && !b.deploymentValues.EnableTerraformDebugMode {
return BuildError{
http.StatusBadRequest,
"Workspace builds with a custom log level are restricted to template authors only.",
xerrors.New("Workspace builds with a custom log level are restricted to template authors only."),
"Terraform debug mode is disabled in the deployment configuration.",
xerrors.New("Terraform debug mode is disabled in the deployment configuration."),
}
}
return nil
Expand Down
15 changes: 13 additions & 2 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ type DeploymentValues struct {
WgtunnelHost clibase.String `json:"wgtunnel_host,omitempty" typescript:",notnull"`
DisableOwnerWorkspaceExec clibase.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"`
ProxyHealthStatusInterval clibase.Duration `json:"proxy_health_status_interval,omitempty" typescript:",notnull"`
EnableTerraformDebugMode clibase.Bool `json:"enable_terraform_debug_mode,omitempty" typescript:",notnull"`

Config clibase.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
WriteConfig clibase.Bool `json:"write_config,omitempty" typescript:",notnull"`
Expand Down Expand Up @@ -1215,10 +1216,20 @@ when required by your organization's security policy.`,
YAML: "stackdriverPath",
Annotations: clibase.Annotations{}.Mark(annotationExternalProxies, "true"),
},
{
Name: "Enable Terraform debug mode",
Description: "Allow administrators to enable Terraform debug output.",
Flag: "enable-terraform-debug-mode",
Env: "CODER_ENABLE_TERRAFORM_DEBUG_MODE",
Default: "false",
Value: &c.EnableTerraformDebugMode,
Group: &deploymentGroupIntrospectionLogging,
YAML: "enableTerraformDebugMode",
},
// ☢️ Dangerous settings
{
Name: "DANGEROUS: Allow all CORs requests",
Description: "For security reasons, CORs requests are blocked except between workspace apps owned by the same user. If external requests are required, setting this to true will set all cors headers as '*'. This should never be used in production.",
Name: "DANGEROUS: Allow all CORS requests",
Description: "For security reasons, CORS requests are blocked except between workspace apps owned by the same user. If external requests are required, setting this to true will set all cors headers as '*'. This should never be used in production.",
Flag: "dangerous-allow-cors-requests",
Env: "CODER_DANGEROUS_ALLOW_CORS_REQUESTS",
Hidden: true, // Hidden, should only be used by yarn dev server
Expand Down
1 change: 1 addition & 0 deletions docs/api/general.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"disable_password_auth": true,
"disable_path_apps": true,
"disable_session_expiry_refresh": true,
"enable_terraform_debug_mode": true,
"experiments": ["string"],
"git_auth": {
"value": [
Expand Down
3 changes: 3 additions & 0 deletions docs/api/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"disable_password_auth": true,
"disable_path_apps": true,
"disable_session_expiry_refresh": true,
"enable_terraform_debug_mode": true,
"experiments": ["string"],
"git_auth": {
"value": [
Expand Down Expand Up @@ -2204,6 +2205,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"disable_password_auth": true,
"disable_path_apps": true,
"disable_session_expiry_refresh": true,
"enable_terraform_debug_mode": true,
"experiments": ["string"],
"git_auth": {
"value": [
Expand Down Expand Up @@ -2398,6 +2400,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `disable_password_auth` | boolean | false | | |
| `disable_path_apps` | boolean | false | | |
| `disable_session_expiry_refresh` | boolean | false | | |
| `enable_terraform_debug_mode` | boolean | false | | |
| `experiments` | array of string | false | | |
| `git_auth` | [clibase.Struct-array_codersdk_GitAuthConfig](#clibasestruct-array_codersdk_gitauthconfig) | false | | |
| `http_address` | string | false | | Http address is a string because it may be set to zero to disable. |
Expand Down
11 changes: 11 additions & 0 deletions docs/cli/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,17 @@ Disable workspace apps that are not served from subdomains. Path-based apps can

Disable automatic session expiry bumping due to activity. This forces all sessions to become invalid after the session expiry duration has been reached.

### --enable-terraform-debug-mode

| | |
| ----------- | ----------------------------------------------------------- |
| Type | <code>bool</code> |
| Environment | <code>$CODER_ENABLE_TERRAFORM_DEBUG_MODE</code> |
| YAML | <code>introspection.logging.enableTerraformDebugMode</code> |
| Default | <code>false</code> |

Allow administrators to enable Terraform debug output.

### --swagger-enable

| | |
Expand Down
3 changes: 3 additions & 0 deletions enterprise/cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ Use a YAML configuration file when your server launch become unwieldy.
Write out the current server config as YAML to stdout.

Introspection / Logging Options
--enable-terraform-debug-mode bool, $CODER_ENABLE_TERRAFORM_DEBUG_MODE (default: false)
Allow administrators to enable Terraform debug output.

--log-human string, $CODER_LOGGING_HUMAN (default: /dev/stderr)
Output human-readable logs to a given file.

Expand Down
1 change: 1 addition & 0 deletions site/src/api/typesGenerated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ export interface DeploymentValues {
readonly wgtunnel_host?: string
readonly disable_owner_workspace_exec?: boolean
readonly proxy_health_status_interval?: number
readonly enable_terraform_debug_mode?: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.YAMLConfigPath")
readonly config?: string
readonly write_config?: boolean
Expand Down
19 changes: 19 additions & 0 deletions site/src/components/Workspace/Workspace.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,25 @@ export const FailedWithLogs: Story = {
},
}

export const FailedWithRetry: Story = {
args: {
...Running.args,
workspace: {
...Mocks.MockFailedWorkspace,
latest_build: {
...Mocks.MockFailedWorkspace.latest_build,
job: {
...Mocks.MockFailedWorkspace.latest_build.job,
error:
"recv workspace provision: plan terraform: terraform plan: exit status 1",
},
},
},
failedBuildLogs: makeFailedBuildLogs(),
canRetryDebugMode: true,
},
}

export const Deleting: Story = {
args: {
...Running.args,
Expand Down
Loading