feat: enable Terraform debug mode via deployment configuration (#8260)

This commit is contained in:
Marcin Tojek
2023-06-29 15:22:21 +02:00
committed by GitHub
parent 7ca624eceb
commit 83fee4b192
19 changed files with 214 additions and 27 deletions
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -7270,6 +7270,9 @@ const docTemplate = `{
"disable_session_expiry_refresh": {
"type": "boolean"
},
"enable_terraform_debug_mode": {
"type": "boolean"
},
"experiments": {
"type": "array",
"items": {
+3
View File
@@ -6493,6 +6493,9 @@
"disable_session_expiry_refresh": {
"type": "boolean"
},
"enable_terraform_debug_mode": {
"type": "boolean"
},
"experiments": {
"type": "array",
"items": {
+2 -1
View File
@@ -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)
+90 -12
View File
@@ -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
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: deploymentValues})
owner := coderdtest.CreateFirstUser(t, adminClient)
// Template author: create a template
version := coderdtest.CreateTemplateVersion(t, adminClient, owner.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, adminClient, owner.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, adminClient, version.ID)
// Template author: create a workspace
workspace := coderdtest.CreateWorkspace(t, adminClient, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, adminClient, 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 := adminClient.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)
@@ -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{
@@ -713,19 +791,19 @@ 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(" "),
@@ -733,10 +811,10 @@ func TestWorkspaceBuildDebugMode(t *testing.T) {
})
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()
+24 -8
View File
@@ -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
@@ -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
@@ -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) {
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."),
"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,
"Terraform debug mode is disabled in the deployment configuration.",
xerrors.New("Terraform debug mode is disabled in the deployment configuration."),
}
}
return nil
+13 -2
View File
@@ -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"`
@@ -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
+1
View File
@@ -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": [
+3
View File
@@ -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": [
@@ -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": [
@@ -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. |
+11
View File
@@ -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
| | |
+3
View File
@@ -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.
+1
View File
@@ -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
@@ -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,
+3 -3
View File
@@ -60,7 +60,7 @@ export interface WorkspaceProps {
builds?: TypesGen.WorkspaceBuild[]
templateWarnings?: TypesGen.TemplateVersionWarning[]
canUpdateWorkspace: boolean
canUpdateTemplate: boolean
canRetryDebugMode: boolean
canChangeVersions: boolean
hideSSHButton?: boolean
hideVSCodeDesktopButton?: boolean
@@ -92,7 +92,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
resources,
builds,
canUpdateWorkspace,
canUpdateTemplate,
canRetryDebugMode,
canChangeVersions,
workspaceErrors,
hideSSHButton,
@@ -236,7 +236,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
<Alert
severity="error"
actions={
canUpdateTemplate && (
canRetryDebugMode && (
<Button
key={0}
onClick={handleBuildRetry}
@@ -23,6 +23,7 @@ import {
MockTemplateVersion3,
MockUser,
MockEntitlementsWithScheduling,
MockDeploymentConfig,
} from "testHelpers/entities"
import * as api from "../../api/api"
import { Workspace } from "../../api/typesGenerated"
@@ -39,6 +40,9 @@ const { t } = i18next
const renderWorkspacePage = async () => {
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate)
jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([])
jest
.spyOn(api, "getDeploymentValues")
.mockResolvedValueOnce(MockDeploymentConfig)
jest.spyOn(api, "watchStartupLogs").mockImplementation((_, options) => {
options.onDone()
return new WebSocket("")
@@ -61,6 +61,7 @@ export const WorkspaceReadyPage = ({
workspace,
template,
templateVersion,
deploymentValues,
builds,
getBuildsError,
buildError,
@@ -75,6 +76,9 @@ export const WorkspaceReadyPage = ({
const deadline = getDeadline(workspace)
const canUpdateWorkspace = Boolean(permissions?.updateWorkspace)
const canUpdateTemplate = Boolean(permissions?.updateTemplate)
const canRetryDebugMode =
Boolean(permissions?.viewDeploymentValues) &&
Boolean(deploymentValues?.enable_terraform_debug_mode)
const { t } = useTranslation("workspacePage")
const favicon = getFaviconByStatus(workspace.latest_build)
const navigate = useNavigate()
@@ -166,7 +170,7 @@ export const WorkspaceReadyPage = ({
resources={workspace.latest_build.resources}
builds={builds}
canUpdateWorkspace={canUpdateWorkspace}
canUpdateTemplate={canUpdateTemplate}
canRetryDebugMode={canRetryDebugMode}
canChangeVersions={canUpdateTemplate}
hideSSHButton={featureVisibility["browser_only"]}
hideVSCodeDesktopButton={featureVisibility["browser_only"]}
+7
View File
@@ -1583,6 +1583,13 @@ export const MockPermissions: Permissions = {
viewDeploymentStats: true,
}
export const MockDeploymentConfig: Types.DeploymentConfig = {
config: {
enable_terraform_debug_mode: true,
},
options: [],
}
export const MockAppearance: TypesGen.AppearanceConfig = {
logo_url: "",
service_banner: {
@@ -55,6 +55,7 @@ export interface WorkspaceContext {
template?: TypesGen.Template
permissions?: Permissions
templateVersion?: TypesGen.TemplateVersion
deploymentValues?: TypesGen.DeploymentValues
build?: TypesGen.WorkspaceBuild
// Builds
builds?: TypesGen.WorkspaceBuild[]
@@ -100,6 +101,7 @@ export const checks = {
readWorkspace: "readWorkspace",
updateWorkspace: "updateWorkspace",
updateTemplate: "updateTemplate",
viewDeploymentValues: "viewDeploymentValues",
} as const
const permissionsToCheck = (
@@ -130,6 +132,12 @@ const permissionsToCheck = (
},
action: "update",
},
[checks.viewDeploymentValues]: {
object: {
resource_type: "deployment_config",
},
action: "read",
},
} as const)
export const workspaceMachine = createMachine(
@@ -488,6 +496,7 @@ export const workspaceMachine = createMachine(
template: (_, event) => event.data.template,
templateVersion: (_, event) => event.data.templateVersion,
permissions: (_, event) => event.data.permissions as Permissions,
deploymentValues: (_, event) => event.data.deploymentValues,
}),
assignError: assign({
error: (_, event) => event.data,
@@ -740,10 +749,17 @@ async function loadInitialWorkspaceData({
}),
])
const canViewDeploymentValues = Boolean(
(permissions as Permissions)?.viewDeploymentValues,
)
const deploymentValues = canViewDeploymentValues
? (await API.getDeploymentValues())?.config
: undefined
return {
workspace,
template,
templateVersion,
permissions,
deploymentValues,
}
}