mirror of https://github.com/coder/coder.git
768 lines
24 KiB
Go
768 lines
24 KiB
Go
// 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"
|
|
"time"
|
|
|
|
"github.com/coder/coder/v2/provisionersdk"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/sqlc-dev/pqtype"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/audit"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"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"
|
|
)
|
|
|
|
// 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
|
|
workspace database.Workspace
|
|
trans database.WorkspaceTransition
|
|
version versionTarget
|
|
state stateTarget
|
|
logLevel string
|
|
deploymentValues *codersdk.DeploymentValues
|
|
|
|
richParameterValues []codersdk.WorkspaceBuildParameter
|
|
initiator uuid.UUID
|
|
reason database.BuildReason
|
|
|
|
// used during build, makes function arguments less verbose
|
|
ctx context.Context
|
|
store database.Store
|
|
|
|
// cache of objects, so we only fetch once
|
|
template *database.Template
|
|
templateVersion *database.TemplateVersion
|
|
templateVersionJob *database.ProvisionerJob
|
|
templateVersionParameters *[]database.TemplateVersionParameter
|
|
lastBuild *database.WorkspaceBuild
|
|
lastBuildErr *error
|
|
lastBuildParameters *[]database.WorkspaceBuildParameter
|
|
lastBuildJob *database.ProvisionerJob
|
|
|
|
verifyNoLegacyParametersOnce bool
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
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,
|
|
authFunc func(action rbac.Action, object rbac.Objecter) bool,
|
|
auditBaggage audit.WorkspaceBuildBaggage,
|
|
) (
|
|
*database.WorkspaceBuild, *database.ProvisionerJob, error,
|
|
) {
|
|
var err error
|
|
b.ctx, err = audit.BaggageToContext(ctx, auditBaggage)
|
|
if err != nil {
|
|
return nil, nil, xerrors.Errorf("create audit baggage: %w", err)
|
|
}
|
|
|
|
// 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.
|
|
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)
|
|
}
|
|
return workspaceBuild, provisionerJob, nil
|
|
}
|
|
|
|
// 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.
|
|
func (b *Builder) buildTx(authFunc func(action rbac.Action, object rbac.Objecter) bool) (
|
|
*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}
|
|
}
|
|
tags := provisionersdk.MutateTags(b.workspace.OwnerID, templateVersionJob.Tags)
|
|
|
|
now := dbtime.Now()
|
|
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}
|
|
}
|
|
|
|
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,
|
|
Deadline: time.Time{}, // set by provisioner upon completion
|
|
MaxDeadline: time.Time{}, // set by provisioner upon completion
|
|
})
|
|
if err != nil {
|
|
code := http.StatusInternalServerError
|
|
if rbac.IsUnauthorizedError(err) {
|
|
code = http.StatusForbidden
|
|
}
|
|
return BuildError{code, "insert workspace build", err}
|
|
}
|
|
|
|
names, values, err := b.getParameters()
|
|
if err != nil {
|
|
// getParameters already wraps errors in BuildError
|
|
return err
|
|
}
|
|
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)
|
|
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) {
|
|
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}
|
|
}
|
|
err = b.verifyNoLegacyParameters()
|
|
if err != nil {
|
|
return nil, nil, BuildError{http.StatusBadRequest, "Unable to build workspace with unsupported parameters", err}
|
|
}
|
|
resolver := codersdk.ParameterResolver{
|
|
Rich: db2sdk.WorkspaceBuildParameters(lastBuildParameters),
|
|
}
|
|
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.)
|
|
return nil, nil, BuildError{http.StatusBadRequest, fmt.Sprintf("Unable to validate parameter %q", templateVersionParameter.Name), err}
|
|
}
|
|
names = append(names, templateVersionParameter.Name)
|
|
values = append(values, value)
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
b.verifyNoLegacyParametersOnce = true
|
|
|
|
// Block starting the workspace with legacy parameters.
|
|
if b.trans != database.WorkspaceTransitionStart {
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// authorize performs build authorization pre-checks using the provided authFunc
|
|
func (b *Builder) authorize(authFunc func(action rbac.Action, object rbac.Objecter) bool) error {
|
|
// 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.
|
|
var action rbac.Action
|
|
switch b.trans {
|
|
case database.WorkspaceTransitionDelete:
|
|
action = rbac.ActionDelete
|
|
case database.WorkspaceTransitionStart, database.WorkspaceTransitionStop:
|
|
action = rbac.ActionUpdate
|
|
default:
|
|
msg := fmt.Sprintf("Transition %q not supported.", b.trans)
|
|
return BuildError{http.StatusBadRequest, msg, xerrors.New(msg)}
|
|
}
|
|
if !authFunc(action, b.workspace) {
|
|
// We use the same wording as the httpapi to avoid leaking the existence of the workspace
|
|
return BuildError{http.StatusNotFound, httpapi.ResourceNotFoundResponse.Message, xerrors.New(httpapi.ResourceNotFoundResponse.Message)}
|
|
}
|
|
|
|
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 {
|
|
if !authFunc(rbac.ActionUpdate, template.RBACObject()) {
|
|
return BuildError{http.StatusForbidden, "Only template managers may provide custom state", xerrors.New("Only template managers may provide custom state")}
|
|
}
|
|
}
|
|
|
|
if b.logLevel != "" && !authFunc(rbac.ActionRead, rbac.ResourceDeploymentValues) {
|
|
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,
|
|
"Terraform debug mode is disabled in the deployment configuration.",
|
|
xerrors.New("Terraform debug mode is disabled in the deployment configuration."),
|
|
}
|
|
}
|
|
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,
|
|
}
|
|
}
|
|
|
|
templateVersionJobStatus := codersdk.ProvisionerJobStatus(templateVersionJob.JobStatus)
|
|
switch templateVersionJobStatus {
|
|
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
|
|
msg := fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus)
|
|
|
|
return BuildError{
|
|
http.StatusNotAcceptable,
|
|
msg,
|
|
xerrors.New(msg),
|
|
}
|
|
case codersdk.ProvisionerJobFailed:
|
|
msg := fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String)
|
|
return BuildError{
|
|
http.StatusBadRequest,
|
|
msg,
|
|
xerrors.New(msg),
|
|
}
|
|
case codersdk.ProvisionerJobCanceled:
|
|
msg := fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String)
|
|
return BuildError{
|
|
http.StatusBadRequest,
|
|
msg,
|
|
xerrors.New(msg),
|
|
}
|
|
}
|
|
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}
|
|
}
|
|
if codersdk.ProvisionerJobStatus(job.JobStatus).Active() {
|
|
msg := "A workspace build is already active."
|
|
return BuildError{
|
|
http.StatusConflict,
|
|
msg,
|
|
xerrors.New(msg),
|
|
}
|
|
}
|
|
return nil
|
|
}
|