2023-05-23 08:06:33 +00:00
// Package wsbuilder provides the Builder object, which encapsulates the common business logic of inserting a new
// workspace build into the database.
package wsbuilder
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"
2023-10-03 08:23:45 +00:00
"time"
2023-05-23 08:06:33 +00:00
2024-05-23 07:53:51 +00:00
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
2024-05-15 14:46:35 +00:00
"github.com/coder/coder/v2/coderd/rbac/policy"
2023-12-08 12:10:25 +00:00
"github.com/coder/coder/v2/provisionersdk"
2023-05-23 08:06:33 +00:00
"github.com/google/uuid"
2023-07-15 06:07:19 +00:00
"github.com/sqlc-dev/pqtype"
2023-05-23 08:06:33 +00:00
"golang.org/x/xerrors"
2023-10-18 20:08:02 +00:00
"github.com/coder/coder/v2/coderd/audit"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
2023-09-01 16:50:12 +00:00
"github.com/coder/coder/v2/coderd/database/dbtime"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/provisionerdserver"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/codersdk"
2023-05-23 08:06:33 +00:00
)
// Builder encapsulates the business logic of inserting a new workspace build into the database.
//
// Builder follows the so-called "Builder" pattern where options that customize the kind of build you get return
// a new instance of the Builder with the option applied.
//
// Example:
//
// b = wsbuilder.New(workspace, transition).VersionID(vID).Initiator(me)
// build, job, err := b.Build(...)
type Builder struct {
// settings that control the kind of build you get
2023-06-29 13:22:21 +00:00
workspace database . Workspace
trans database . WorkspaceTransition
version versionTarget
state stateTarget
logLevel string
deploymentValues * codersdk . DeploymentValues
2023-06-02 09:16:46 +00:00
richParameterValues [ ] codersdk . WorkspaceBuildParameter
initiator uuid . UUID
reason database . BuildReason
2023-05-23 08:06:33 +00:00
// used during build, makes function arguments less verbose
ctx context . Context
store database . Store
// cache of objects, so we only fetch once
2024-05-23 07:53:51 +00:00
template * database . Template
templateVersion * database . TemplateVersion
templateVersionJob * database . ProvisionerJob
templateVersionParameters * [ ] database . TemplateVersionParameter
templateVersionWorkspaceTags * [ ] database . TemplateVersionWorkspaceTag
lastBuild * database . WorkspaceBuild
lastBuildErr * error
lastBuildParameters * [ ] database . WorkspaceBuildParameter
lastBuildJob * database . ProvisionerJob
parameterNames * [ ] string
parameterValues * [ ] string
2023-06-02 09:16:46 +00:00
verifyNoLegacyParametersOnce bool
2023-05-23 08:06:33 +00:00
}
type Option func ( Builder ) Builder
// versionTarget expresses how to determine the template version for the build.
//
// The zero value of this struct means to use the version from the last build. If there is no last build,
// the build will fail.
//
// setting active: true means to use the active version from the template.
//
// setting specific to a non-nil value means to use the provided template version ID.
//
// active and specific are mutually exclusive and setting them both results in undefined behavior.
type versionTarget struct {
active bool
specific * uuid . UUID
}
// stateTarget expresses how to determine the provisioner state for the build.
//
// The zero value of this struct means to use state from the last build. If there is no last build, no state is
// provided (i.e. first build on a newly created workspace).
//
// setting orphan: true means not to send any state. This can be used to deleted orphaned workspaces
//
// setting explicit to a non-nil value means to use the provided state
//
// orphan and explicit are mutually exclusive and setting them both results in undefined behavior.
type stateTarget struct {
orphan bool
explicit * [ ] byte
}
func New ( w database . Workspace , t database . WorkspaceTransition ) Builder {
return Builder { workspace : w , trans : t }
}
// Methods that customize the build are public, have a struct receiver and return a new Builder.
func ( b Builder ) VersionID ( v uuid . UUID ) Builder {
// nolint: revive
b . version = versionTarget { specific : & v }
return b
}
func ( b Builder ) ActiveVersion ( ) Builder {
// nolint: revive
b . version = versionTarget { active : true }
return b
}
func ( b Builder ) State ( state [ ] byte ) Builder {
// nolint: revive
b . state = stateTarget { explicit : & state }
return b
}
func ( b Builder ) Orphan ( ) Builder {
// nolint: revive
b . state = stateTarget { orphan : true }
return b
}
func ( b Builder ) LogLevel ( l string ) Builder {
// nolint: revive
b . logLevel = l
return b
}
2023-06-29 13:22:21 +00:00
func ( b Builder ) DeploymentValues ( dv * codersdk . DeploymentValues ) Builder {
// nolint: revive
b . deploymentValues = dv
return b
}
2023-05-23 08:06:33 +00:00
func ( b Builder ) Initiator ( u uuid . UUID ) Builder {
// nolint: revive
b . initiator = u
return b
}
func ( b Builder ) Reason ( r database . BuildReason ) Builder {
// nolint: revive
b . reason = r
return b
}
func ( b Builder ) RichParameterValues ( p [ ] codersdk . WorkspaceBuildParameter ) Builder {
// nolint: revive
b . richParameterValues = p
return b
}
// SetLastWorkspaceBuildInTx prepopulates the Builder's cache with the last workspace build. This allows us
// to avoid a repeated database query when the Builder's caller also needs the workspace build, e.g. auto-start &
// auto-stop.
//
// CAUTION: only call this method from within a database transaction with RepeatableRead isolation. This transaction
// MUST be the database.Store you call Build() with.
func ( b Builder ) SetLastWorkspaceBuildInTx ( build * database . WorkspaceBuild ) Builder {
// nolint: revive
b . lastBuild = build
return b
}
// SetLastWorkspaceBuildJobInTx prepopulates the Builder's cache with the last workspace build job. This allows us
// to avoid a repeated database query when the Builder's caller also needs the workspace build job, e.g. auto-start &
// auto-stop.
//
// CAUTION: only call this method from within a database transaction with RepeatableRead isolation. This transaction
// MUST be the database.Store you call Build() with.
func ( b Builder ) SetLastWorkspaceBuildJobInTx ( job * database . ProvisionerJob ) Builder {
// nolint: revive
b . lastBuildJob = job
return b
}
type BuildError struct {
// Status is a suitable HTTP status code
Status int
Message string
Wrapped error
}
func ( e BuildError ) Error ( ) string {
return e . Wrapped . Error ( )
}
func ( e BuildError ) Unwrap ( ) error {
return e . Wrapped
}
// Build computes and inserts a new workspace build into the database. If authFunc is provided, it also performs
// authorization preflight checks.
func ( b * Builder ) Build (
ctx context . Context ,
store database . Store ,
2024-05-15 14:46:35 +00:00
authFunc func ( action policy . Action , object rbac . Objecter ) bool ,
2023-10-18 20:08:02 +00:00
auditBaggage audit . WorkspaceBuildBaggage ,
2023-05-23 08:06:33 +00:00
) (
* database . WorkspaceBuild , * database . ProvisionerJob , error ,
) {
2023-10-18 20:08:02 +00:00
var err error
b . ctx , err = audit . BaggageToContext ( ctx , auditBaggage )
if err != nil {
return nil , nil , xerrors . Errorf ( "create audit baggage: %w" , err )
}
2023-05-23 08:06:33 +00:00
// Run the build in a transaction with RepeatableRead isolation, and retries.
// RepeatableRead isolation ensures that we get a consistent view of the database while
// computing the new build. This simplifies the logic so that we do not need to worry if
// later reads are consistent with earlier ones.
2023-10-18 22:07:21 +00:00
var workspaceBuild * database . WorkspaceBuild
var provisionerJob * database . ProvisionerJob
err = database . ReadModifyUpdate ( store , func ( tx database . Store ) error {
var err error
b . store = tx
workspaceBuild , provisionerJob , err = b . buildTx ( authFunc )
return err
} )
if err != nil {
return nil , nil , xerrors . Errorf ( "build tx: %w" , err )
2023-05-23 08:06:33 +00:00
}
2023-10-18 22:07:21 +00:00
return workspaceBuild , provisionerJob , nil
2023-05-23 08:06:33 +00:00
}
// buildTx contains the business logic of computing a new build. Attributes of the new database objects are computed
// in a functional style, rather than imperative, to emphasize the logic of how they are defined. A simple cache
// of database-fetched objects is stored on the struct to ensure we only fetch things once, even if they are used in
// the calculation of multiple attributes.
//
// In order to utilize this cache, the functions that compute build attributes use a pointer receiver type.
2024-05-15 14:46:35 +00:00
func ( b * Builder ) buildTx ( authFunc func ( action policy . Action , object rbac . Objecter ) bool ) (
2023-05-23 08:06:33 +00:00
* database . WorkspaceBuild , * database . ProvisionerJob , error ,
) {
if authFunc != nil {
err := b . authorize ( authFunc )
if err != nil {
return nil , nil , err
}
}
err := b . checkTemplateVersionMatchesTemplate ( )
if err != nil {
return nil , nil , err
}
err = b . checkTemplateJobStatus ( )
if err != nil {
return nil , nil , err
}
err = b . checkRunningBuild ( )
if err != nil {
return nil , nil , err
}
template , err := b . getTemplate ( )
if err != nil {
return nil , nil , BuildError { http . StatusInternalServerError , "failed to fetch template" , err }
}
templateVersionJob , err := b . getTemplateVersionJob ( )
if err != nil {
return nil , nil , BuildError {
http . StatusInternalServerError , "failed to fetch template version job" , err ,
}
}
// if we haven't been told specifically who initiated, default to owner
if b . initiator == uuid . Nil {
b . initiator = b . workspace . OwnerID
}
// default reason is initiator
if b . reason == "" {
b . reason = database . BuildReasonInitiator
}
workspaceBuildID := uuid . New ( )
input , err := json . Marshal ( provisionerdserver . WorkspaceProvisionJob {
WorkspaceBuildID : workspaceBuildID ,
LogLevel : b . logLevel ,
} )
if err != nil {
return nil , nil , BuildError {
http . StatusInternalServerError ,
"marshal provision job" ,
err ,
}
}
traceMetadataRaw , err := json . Marshal ( tracing . MetadataFromContext ( b . ctx ) )
if err != nil {
return nil , nil , BuildError { http . StatusInternalServerError , "marshal metadata" , err }
}
2024-05-23 07:53:51 +00:00
tags , err := b . getProvisionerTags ( )
if err != nil {
return nil , nil , err // already wrapped BuildError
}
2023-05-23 08:06:33 +00:00
2023-09-01 16:50:12 +00:00
now := dbtime . Now ( )
2023-05-23 08:06:33 +00:00
provisionerJob , err := b . store . InsertProvisionerJob ( b . ctx , database . InsertProvisionerJobParams {
ID : uuid . New ( ) ,
CreatedAt : now ,
UpdatedAt : now ,
InitiatorID : b . initiator ,
OrganizationID : template . OrganizationID ,
Provisioner : template . Provisioner ,
Type : database . ProvisionerJobTypeWorkspaceBuild ,
StorageMethod : templateVersionJob . StorageMethod ,
FileID : templateVersionJob . FileID ,
Input : input ,
Tags : tags ,
TraceMetadata : pqtype . NullRawMessage {
Valid : true ,
RawMessage : traceMetadataRaw ,
} ,
} )
if err != nil {
return nil , nil , BuildError { http . StatusInternalServerError , "insert provisioner job" , err }
}
templateVersionID , err := b . getTemplateVersionID ( )
if err != nil {
return nil , nil , BuildError { http . StatusInternalServerError , "compute template version ID" , err }
}
buildNum , err := b . getBuildNumber ( )
if err != nil {
return nil , nil , BuildError { http . StatusInternalServerError , "compute build number" , err }
}
state , err := b . getState ( )
if err != nil {
return nil , nil , BuildError { http . StatusInternalServerError , "compute build state" , err }
}
2023-07-25 13:14:38 +00:00
var workspaceBuild database . WorkspaceBuild
err = b . store . InTx ( func ( store database . Store ) error {
err = store . InsertWorkspaceBuild ( b . ctx , database . InsertWorkspaceBuildParams {
ID : workspaceBuildID ,
CreatedAt : now ,
UpdatedAt : now ,
WorkspaceID : b . workspace . ID ,
TemplateVersionID : templateVersionID ,
BuildNumber : buildNum ,
ProvisionerState : state ,
InitiatorID : b . initiator ,
Transition : b . trans ,
JobID : provisionerJob . ID ,
Reason : b . reason ,
2023-10-03 08:23:45 +00:00
Deadline : time . Time { } , // set by provisioner upon completion
MaxDeadline : time . Time { } , // set by provisioner upon completion
2023-07-25 13:14:38 +00:00
} )
if err != nil {
2023-10-18 22:07:21 +00:00
code := http . StatusInternalServerError
if rbac . IsUnauthorizedError ( err ) {
2023-12-09 05:03:46 +00:00
code = http . StatusForbidden
2023-10-18 22:07:21 +00:00
}
return BuildError { code , "insert workspace build" , err }
2023-07-25 13:14:38 +00:00
}
names , values , err := b . getParameters ( )
if err != nil {
// getParameters already wraps errors in BuildError
return err
}
2024-05-23 07:53:51 +00:00
2023-07-25 13:14:38 +00:00
err = store . InsertWorkspaceBuildParameters ( b . ctx , database . InsertWorkspaceBuildParametersParams {
WorkspaceBuildID : workspaceBuildID ,
Name : names ,
Value : values ,
} )
if err != nil {
return BuildError { http . StatusInternalServerError , "insert workspace build parameters: %w" , err }
}
workspaceBuild , err = store . GetWorkspaceBuildByID ( b . ctx , workspaceBuildID )
if err != nil {
return BuildError { http . StatusInternalServerError , "get workspace build" , err }
}
return nil
} , nil )
2023-05-23 08:06:33 +00:00
if err != nil {
return nil , nil , err
}
return & workspaceBuild , & provisionerJob , nil
}
func ( b * Builder ) getTemplate ( ) ( * database . Template , error ) {
if b . template != nil {
return b . template , nil
}
t , err := b . store . GetTemplateByID ( b . ctx , b . workspace . TemplateID )
if err != nil {
return nil , xerrors . Errorf ( "get template %s: %w" , b . workspace . TemplateID , err )
}
b . template = & t
return b . template , nil
}
func ( b * Builder ) getTemplateVersionJob ( ) ( * database . ProvisionerJob , error ) {
if b . templateVersionJob != nil {
return b . templateVersionJob , nil
}
v , err := b . getTemplateVersion ( )
if err != nil {
return nil , xerrors . Errorf ( "get template version so we can get provisioner job: %w" , err )
}
j , err := b . store . GetProvisionerJobByID ( b . ctx , v . JobID )
if err != nil {
return nil , xerrors . Errorf ( "get template provisioner job %s: %w" , v . JobID , err )
}
b . templateVersionJob = & j
return b . templateVersionJob , err
}
func ( b * Builder ) getTemplateVersion ( ) ( * database . TemplateVersion , error ) {
if b . templateVersion != nil {
return b . templateVersion , nil
}
id , err := b . getTemplateVersionID ( )
if err != nil {
return nil , xerrors . Errorf ( "get template version ID so we can get version: %w" , err )
}
v , err := b . store . GetTemplateVersionByID ( b . ctx , id )
if err != nil {
return nil , xerrors . Errorf ( "get template version %s: %w" , id , err )
}
b . templateVersion = & v
return b . templateVersion , err
}
func ( b * Builder ) getTemplateVersionID ( ) ( uuid . UUID , error ) {
if b . version . specific != nil {
return * b . version . specific , nil
}
if b . version . active {
t , err := b . getTemplate ( )
if err != nil {
return uuid . Nil , xerrors . Errorf ( "get template so we can get active version: %w" , err )
}
return t . ActiveVersionID , nil
}
// default is prior version
bld , err := b . getLastBuild ( )
if err != nil {
return uuid . Nil , xerrors . Errorf ( "get last build so we can get version: %w" , err )
}
return bld . TemplateVersionID , nil
}
func ( b * Builder ) getLastBuild ( ) ( * database . WorkspaceBuild , error ) {
if b . lastBuild != nil {
return b . lastBuild , nil
}
// last build might not exist, so we also store the error to prevent repeated queries
// for a non-existing build
if b . lastBuildErr != nil {
return nil , * b . lastBuildErr
}
bld , err := b . store . GetLatestWorkspaceBuildByWorkspaceID ( b . ctx , b . workspace . ID )
if err != nil {
err = xerrors . Errorf ( "get workspace %s last build: %w" , b . workspace . ID , err )
b . lastBuildErr = & err
return nil , err
}
b . lastBuild = & bld
return b . lastBuild , nil
}
func ( b * Builder ) getBuildNumber ( ) ( int32 , error ) {
bld , err := b . getLastBuild ( )
if xerrors . Is ( err , sql . ErrNoRows ) {
// first build!
return 1 , nil
}
if err != nil {
return 0 , xerrors . Errorf ( "get last build to compute build number: %w" , err )
}
return bld . BuildNumber + 1 , nil
}
func ( b * Builder ) getState ( ) ( [ ] byte , error ) {
if b . state . orphan {
// Orphan means empty state.
return nil , nil
}
if b . state . explicit != nil {
return * b . state . explicit , nil
}
// Default is to use state from prior build
bld , err := b . getLastBuild ( )
if xerrors . Is ( err , sql . ErrNoRows ) {
// last build does not exist, which implies empty state
return nil , nil
}
if err != nil {
return nil , xerrors . Errorf ( "get last build to get state: %w" , err )
}
return bld . ProvisionerState , nil
}
func ( b * Builder ) getParameters ( ) ( names , values [ ] string , err error ) {
2024-05-23 07:53:51 +00:00
if b . parameterNames != nil {
return * b . parameterNames , * b . parameterValues , nil
}
2023-05-23 08:06:33 +00:00
templateVersionParameters , err := b . getTemplateVersionParameters ( )
if err != nil {
return nil , nil , BuildError { http . StatusInternalServerError , "failed to fetch template version parameters" , err }
}
lastBuildParameters , err := b . getLastBuildParameters ( )
if err != nil {
return nil , nil , BuildError { http . StatusInternalServerError , "failed to fetch last build parameters" , err }
}
2023-06-02 09:16:46 +00:00
err = b . verifyNoLegacyParameters ( )
2023-05-23 08:06:33 +00:00
if err != nil {
2023-06-02 09:16:46 +00:00
return nil , nil , BuildError { http . StatusBadRequest , "Unable to build workspace with unsupported parameters" , err }
2023-05-23 08:06:33 +00:00
}
resolver := codersdk . ParameterResolver {
2023-06-02 09:16:46 +00:00
Rich : db2sdk . WorkspaceBuildParameters ( lastBuildParameters ) ,
2023-05-23 08:06:33 +00:00
}
for _ , templateVersionParameter := range templateVersionParameters {
tvp , err := db2sdk . TemplateVersionParameter ( templateVersionParameter )
if err != nil {
return nil , nil , BuildError { http . StatusInternalServerError , "failed to convert template version parameter" , err }
}
value , err := resolver . ValidateResolve (
tvp ,
b . findNewBuildParameterValue ( templateVersionParameter . Name ) ,
)
if err != nil {
// At this point, we've queried all the data we need from the database,
// so the only errors are problems with the request (missing data, failed
// validation, immutable parameters, etc.)
2023-08-23 16:18:38 +00:00
return nil , nil , BuildError { http . StatusBadRequest , fmt . Sprintf ( "Unable to validate parameter %q" , templateVersionParameter . Name ) , err }
2023-05-23 08:06:33 +00:00
}
names = append ( names , templateVersionParameter . Name )
values = append ( values , value )
}
2024-05-23 07:53:51 +00:00
b . parameterNames = & names
b . parameterValues = & values
2023-05-23 08:06:33 +00:00
return names , values , nil
}
func ( b * Builder ) findNewBuildParameterValue ( name string ) * codersdk . WorkspaceBuildParameter {
for _ , v := range b . richParameterValues {
if v . Name == name {
return & v
}
}
return nil
}
func ( b * Builder ) getLastBuildParameters ( ) ( [ ] database . WorkspaceBuildParameter , error ) {
if b . lastBuildParameters != nil {
return * b . lastBuildParameters , nil
}
bld , err := b . getLastBuild ( )
if xerrors . Is ( err , sql . ErrNoRows ) {
// if the build doesn't exist, then clearly there can be no parameters.
b . lastBuildParameters = & [ ] database . WorkspaceBuildParameter { }
return * b . lastBuildParameters , nil
}
if err != nil {
return nil , xerrors . Errorf ( "get last build to get parameters: %w" , err )
}
values , err := b . store . GetWorkspaceBuildParameters ( b . ctx , bld . ID )
if err != nil && ! xerrors . Is ( err , sql . ErrNoRows ) {
return nil , xerrors . Errorf ( "get last build %s parameters: %w" , bld . ID , err )
}
b . lastBuildParameters = & values
return values , nil
}
func ( b * Builder ) getTemplateVersionParameters ( ) ( [ ] database . TemplateVersionParameter , error ) {
if b . templateVersionParameters != nil {
return * b . templateVersionParameters , nil
}
tvID , err := b . getTemplateVersionID ( )
if err != nil {
return nil , xerrors . Errorf ( "get template version ID to get parameters: %w" , err )
}
tvp , err := b . store . GetTemplateVersionParameters ( b . ctx , tvID )
if err != nil && ! xerrors . Is ( err , sql . ErrNoRows ) {
return nil , xerrors . Errorf ( "get template version %s parameters: %w" , tvID , err )
}
b . templateVersionParameters = & tvp
return tvp , nil
}
2023-06-02 09:16:46 +00:00
// verifyNoLegacyParameters verifies that initiator can't start the workspace build
// if it uses legacy parameters (database.ParameterSchemas).
func ( b * Builder ) verifyNoLegacyParameters ( ) error {
if b . verifyNoLegacyParametersOnce {
return nil
2023-05-23 08:06:33 +00:00
}
2023-06-02 09:16:46 +00:00
b . verifyNoLegacyParametersOnce = true
// Block starting the workspace with legacy parameters.
if b . trans != database . WorkspaceTransitionStart {
return nil
2023-05-23 08:06:33 +00:00
}
2023-06-02 09:16:46 +00:00
templateVersionJob , err := b . getTemplateVersionJob ( )
if err != nil {
return xerrors . Errorf ( "failed to fetch template version job: %w" , err )
}
parameterSchemas , err := b . store . GetParameterSchemasByJobID ( b . ctx , templateVersionJob . ID )
if xerrors . Is ( err , sql . ErrNoRows ) {
return nil
}
if err != nil {
return xerrors . Errorf ( "failed to get parameter schemas: %w" , err )
}
if len ( parameterSchemas ) > 0 {
return xerrors . Errorf ( "Legacy parameters in use on this version are not supported anymore. Contact your administrator for assistance." )
}
return nil
2023-05-23 08:06:33 +00:00
}
func ( b * Builder ) getLastBuildJob ( ) ( * database . ProvisionerJob , error ) {
if b . lastBuildJob != nil {
return b . lastBuildJob , nil
}
bld , err := b . getLastBuild ( )
if err != nil {
return nil , xerrors . Errorf ( "get last build to get job: %w" , err )
}
job , err := b . store . GetProvisionerJobByID ( b . ctx , bld . JobID )
if err != nil {
return nil , xerrors . Errorf ( "get build provisioner job %s: %w" , bld . JobID , err )
}
b . lastBuildJob = & job
return b . lastBuildJob , nil
}
2024-05-23 07:53:51 +00:00
func ( b * Builder ) getProvisionerTags ( ) ( map [ string ] string , error ) {
// Step 1: Mutate template version tags
templateVersionJob , err := b . getTemplateVersionJob ( )
if err != nil {
return nil , BuildError { http . StatusInternalServerError , "failed to fetch template version job" , err }
}
annotationTags := provisionersdk . MutateTags ( b . workspace . OwnerID , templateVersionJob . Tags )
tags := map [ string ] string { }
for name , value := range annotationTags {
tags [ name ] = value
}
// Step 2: Mutate workspace tags
workspaceTags , err := b . getTemplateVersionWorkspaceTags ( )
if err != nil {
return nil , BuildError { http . StatusInternalServerError , "failed to fetch template version workspace tags" , err }
}
parameterNames , parameterValues , err := b . getParameters ( )
if err != nil {
return nil , err // already wrapped BuildError
}
evalCtx := buildParametersEvalContext ( parameterNames , parameterValues )
for _ , workspaceTag := range workspaceTags {
expr , diags := hclsyntax . ParseExpression ( [ ] byte ( workspaceTag . Value ) , "expression.hcl" , hcl . InitialPos )
if diags . HasErrors ( ) {
return nil , BuildError { http . StatusBadRequest , "failed to parse workspace tag value" , xerrors . Errorf ( diags . Error ( ) ) }
}
val , diags := expr . Value ( evalCtx )
if diags . HasErrors ( ) {
return nil , BuildError { http . StatusBadRequest , "failed to evaluate workspace tag value" , xerrors . Errorf ( diags . Error ( ) ) }
}
// Do not use "val.AsString()" as it can panic
str , err := ctyValueString ( val )
if err != nil {
return nil , BuildError { http . StatusBadRequest , "failed to marshal cty.Value as string" , err }
}
tags [ workspaceTag . Key ] = str
}
return tags , nil
}
func buildParametersEvalContext ( names , values [ ] string ) * hcl . EvalContext {
m := map [ string ] cty . Value { }
for i , name := range names {
m [ name ] = cty . MapVal ( map [ string ] cty . Value {
"value" : cty . StringVal ( values [ i ] ) ,
} )
}
if len ( m ) == 0 {
return nil // otherwise, panic: must not call MapVal with empty map
}
return & hcl . EvalContext {
Variables : map [ string ] cty . Value {
"data" : cty . MapVal ( map [ string ] cty . Value {
"coder_parameter" : cty . MapVal ( m ) ,
} ) ,
} ,
}
}
func ctyValueString ( val cty . Value ) ( string , error ) {
switch val . Type ( ) {
case cty . Bool :
if val . True ( ) {
return "true" , nil
} else {
return "false" , nil
}
case cty . Number :
return val . AsBigFloat ( ) . String ( ) , nil
case cty . String :
return val . AsString ( ) , nil
default :
return "" , xerrors . Errorf ( "only primitive types are supported - bool, number, and string" )
}
}
func ( b * Builder ) getTemplateVersionWorkspaceTags ( ) ( [ ] database . TemplateVersionWorkspaceTag , error ) {
if b . templateVersionWorkspaceTags != nil {
return * b . templateVersionWorkspaceTags , nil
}
templateVersion , err := b . getTemplateVersion ( )
if err != nil {
return nil , xerrors . Errorf ( "get template version: %w" , err )
}
workspaceTags , err := b . store . GetTemplateVersionWorkspaceTags ( b . ctx , templateVersion . ID )
if err != nil && ! xerrors . Is ( err , sql . ErrNoRows ) {
return nil , xerrors . Errorf ( "get template version workspace tags: %w" , err )
}
b . templateVersionWorkspaceTags = & workspaceTags
return * b . templateVersionWorkspaceTags , nil
}
2023-05-23 08:06:33 +00:00
// authorize performs build authorization pre-checks using the provided authFunc
2024-05-15 14:46:35 +00:00
func ( b * Builder ) authorize ( authFunc func ( action policy . Action , object rbac . Objecter ) bool ) error {
2023-05-23 08:06:33 +00:00
// Doing this up front saves a lot of work if the user doesn't have permission.
// This is checked again in the dbauthz layer, but the check is cached
// and will be a noop later.
2024-05-15 14:46:35 +00:00
var action policy . Action
2023-05-23 08:06:33 +00:00
switch b . trans {
case database . WorkspaceTransitionDelete :
2024-05-15 14:46:35 +00:00
action = policy . ActionDelete
2023-05-23 08:06:33 +00:00
case database . WorkspaceTransitionStart , database . WorkspaceTransitionStop :
2024-05-15 14:46:35 +00:00
action = policy . ActionUpdate
2023-05-23 08:06:33 +00:00
default :
2023-06-22 04:33:22 +00:00
msg := fmt . Sprintf ( "Transition %q not supported." , b . trans )
return BuildError { http . StatusBadRequest , msg , xerrors . New ( msg ) }
2023-05-23 08:06:33 +00:00
}
if ! authFunc ( action , b . workspace ) {
// We use the same wording as the httpapi to avoid leaking the existence of the workspace
2023-06-22 04:33:22 +00:00
return BuildError { http . StatusNotFound , httpapi . ResourceNotFoundResponse . Message , xerrors . New ( httpapi . ResourceNotFoundResponse . Message ) }
2023-05-23 08:06:33 +00:00
}
template , err := b . getTemplate ( )
if err != nil {
return BuildError { http . StatusInternalServerError , "failed to fetch template" , err }
}
// If custom state, deny request since user could be corrupting or leaking
// cloud state.
if b . state . explicit != nil || b . state . orphan {
2024-05-15 14:46:35 +00:00
if ! authFunc ( policy . ActionUpdate , template . RBACObject ( ) ) {
2023-06-22 04:33:22 +00:00
return BuildError { http . StatusForbidden , "Only template managers may provide custom state" , xerrors . New ( "Only template managers may provide custom state" ) }
2023-05-23 08:06:33 +00:00
}
}
2024-05-15 16:09:42 +00:00
if b . logLevel != "" && ! authFunc ( policy . ActionRead , rbac . ResourceDeploymentConfig ) {
2023-06-29 13:22:21 +00:00
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 {
2023-05-23 08:06:33 +00:00
return BuildError {
http . StatusBadRequest ,
2023-06-29 13:22:21 +00:00
"Terraform debug mode is disabled in the deployment configuration." ,
xerrors . New ( "Terraform debug mode is disabled in the deployment configuration." ) ,
2023-05-23 08:06:33 +00:00
}
}
return nil
}
func ( b * Builder ) checkTemplateVersionMatchesTemplate ( ) error {
template , err := b . getTemplate ( )
if err != nil {
return BuildError { http . StatusInternalServerError , "failed to fetch template" , err }
}
templateVersion , err := b . getTemplateVersion ( )
if xerrors . Is ( err , sql . ErrNoRows ) {
return BuildError { http . StatusBadRequest , "template version does not exist" , err }
}
if err != nil {
return BuildError { http . StatusInternalServerError , "failed to fetch template version" , err }
}
if ! templateVersion . TemplateID . Valid || templateVersion . TemplateID . UUID != template . ID {
return BuildError {
http . StatusBadRequest ,
"template version doesn't match template" ,
xerrors . Errorf ( "templateVersion.TemplateID = %+v, template.ID = %s" ,
templateVersion . TemplateID , template . ID ) ,
}
}
return nil
}
func ( b * Builder ) checkTemplateJobStatus ( ) error {
templateVersion , err := b . getTemplateVersion ( )
if err != nil {
return BuildError { http . StatusInternalServerError , "failed to fetch template version" , err }
}
templateVersionJob , err := b . getTemplateVersionJob ( )
if err != nil {
return BuildError {
http . StatusInternalServerError , "failed to fetch template version job" , err ,
}
}
2023-10-05 01:57:46 +00:00
templateVersionJobStatus := codersdk . ProvisionerJobStatus ( templateVersionJob . JobStatus )
2023-05-23 08:06:33 +00:00
switch templateVersionJobStatus {
case codersdk . ProvisionerJobPending , codersdk . ProvisionerJobRunning :
2023-06-22 04:33:22 +00:00
msg := fmt . Sprintf ( "The provided template version is %s. Wait for it to complete importing!" , templateVersionJobStatus )
2023-05-23 08:06:33 +00:00
return BuildError {
http . StatusNotAcceptable ,
2023-06-22 04:33:22 +00:00
msg ,
xerrors . New ( msg ) ,
2023-05-23 08:06:33 +00:00
}
case codersdk . ProvisionerJobFailed :
2023-06-22 04:33:22 +00:00
msg := fmt . Sprintf ( "The provided template version %q has failed to import: %q. You cannot build workspaces with it!" , templateVersion . Name , templateVersionJob . Error . String )
2023-05-23 08:06:33 +00:00
return BuildError {
http . StatusBadRequest ,
2023-06-22 04:33:22 +00:00
msg ,
xerrors . New ( msg ) ,
2023-05-23 08:06:33 +00:00
}
case codersdk . ProvisionerJobCanceled :
2023-06-22 04:33:22 +00:00
msg := fmt . Sprintf ( "The provided template version %q has failed to import: %q. You cannot build workspaces with it!" , templateVersion . Name , templateVersionJob . Error . String )
2023-05-23 08:06:33 +00:00
return BuildError {
http . StatusBadRequest ,
2023-06-22 04:33:22 +00:00
msg ,
xerrors . New ( msg ) ,
2023-05-23 08:06:33 +00:00
}
}
return nil
}
func ( b * Builder ) checkRunningBuild ( ) error {
job , err := b . getLastBuildJob ( )
if xerrors . Is ( err , sql . ErrNoRows ) {
// no prior build, so it can't be running!
return nil
}
if err != nil {
return BuildError { http . StatusInternalServerError , "failed to fetch prior build" , err }
}
2023-10-05 01:57:46 +00:00
if codersdk . ProvisionerJobStatus ( job . JobStatus ) . Active ( ) {
2023-06-22 04:33:22 +00:00
msg := "A workspace build is already active."
2023-05-23 08:06:33 +00:00
return BuildError {
http . StatusConflict ,
2023-06-22 04:33:22 +00:00
msg ,
xerrors . New ( msg ) ,
2023-05-23 08:06:33 +00:00
}
}
return nil
}