refactor: workspace builds (#7541)

* refactor workspace builds

Signed-off-by: Spike Curtis <spike@coder.com>

* make gen

Signed-off-by: Spike Curtis <spike@coder.com>

* Remove ParameterResolver from typescript

Signed-off-by: Spike Curtis <spike@coder.com>

* rename conversion -> database/db2sdk

Signed-off-by: Spike Curtis <spike@coder.com>

* tests for db2sdk

Signed-off-by: Spike Curtis <spike@coder.com>

* Tests for ParameterResolver

Signed-off-by: Spike Curtis <spike@coder.com>

* wsbuilder tests

Signed-off-by: Spike Curtis <spike@coder.com>

* Move parameter validation tests to richparameters_test.go

Signed-off-by: Spike Curtis <spike@coder.com>

* Fix CI generation; rename mock->dbmock

Signed-off-by: Spike Curtis <spike@coder.com>

* Fix test imports

Signed-off-by: Spike Curtis <spike@coder.com>

---------

Signed-off-by: Spike Curtis <spike@coder.com>
This commit is contained in:
Spike Curtis 2023-05-23 12:06:33 +04:00 committed by GitHub
parent 622456faf8
commit cd416c86dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 5681 additions and 1238 deletions

View File

@ -184,6 +184,8 @@ jobs:
run: go install golang.org/x/tools/cmd/goimports@latest
- name: Install yq
run: go run github.com/mikefarah/yq/v4@v4.30.6
- name: Install mockgen
run: go install github.com/golang/mock/mockgen@v1.6.0
- name: Install Protoc
run: |

View File

@ -420,6 +420,7 @@ lint/shellcheck: $(SHELL_SRC_FILES)
gen: \
coderd/database/dump.sql \
coderd/database/querier.go \
coderd/database/dbmock/store.go \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
site/src/api/typesGenerated.ts \
@ -441,6 +442,7 @@ gen/mark-fresh:
files="\
coderd/database/dump.sql \
coderd/database/querier.go \
coderd/database/dbmock/store.go \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
site/src/api/typesGenerated.ts \
@ -476,6 +478,10 @@ coderd/database/dump.sql: coderd/database/gen/dump/main.go $(wildcard coderd/dat
coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql) coderd/database/gen/enum/main.go
./coderd/database/generate.sh
coderd/database/dbmock/store.go: coderd/database/db.go coderd/database/querier.go
go generate ./coderd/database/dbmock/
provisionersdk/proto/provisioner.pb.go: provisionersdk/proto/provisioner.proto
protoc \
--go_out=. \

View File

@ -51,15 +51,13 @@ func TestTemplateDelete(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
templates := []codersdk.Template{
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID),
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID),
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID),
}
templates := []codersdk.Template{}
templateNames := []string{}
for _, template := range templates {
for i := 0; i < 3; i++ {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
templates = append(templates, template)
templateNames = append(templateNames, template.Name)
}
@ -78,15 +76,13 @@ func TestTemplateDelete(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
templates := []codersdk.Template{
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID),
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID),
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID),
}
templates := []codersdk.Template{}
templateNames := []string{}
for _, template := range templates {
for i := 0; i < 3; i++ {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
templates = append(templates, template)
templateNames = append(templateNames, template.Name)
}

3
coderd/apidoc/docs.go generated
View File

@ -8063,6 +8063,9 @@ const docTemplate = `{
}
]
},
"source_value": {
"type": "string"
},
"updated_at": {
"type": "string",
"format": "date-time"

View File

@ -7206,6 +7206,9 @@
}
]
},
"source_value": {
"type": "string"
},
"updated_at": {
"type": "string",
"format": "date-time"

View File

@ -2,7 +2,7 @@ package executor
import (
"context"
"encoding/json"
"database/sql"
"sync/atomic"
"time"
@ -13,8 +13,8 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/wsbuilder"
)
// Executor automatically starts or stops workspaces.
@ -168,20 +168,35 @@ func (e *Executor) runOnce(t time.Time) Stats {
)
return nil
}
builder := wsbuilder.New(ws, validTransition).
SetLastWorkspaceBuildInTx(&priorHistory).
SetLastWorkspaceBuildJobInTx(&priorJob)
log.Info(e.ctx, "scheduling workspace transition", slog.F("transition", validTransition))
stats.Transitions[ws.ID] = validTransition
if err := build(e.ctx, db, ws, validTransition, priorHistory, priorJob); err != nil {
switch validTransition {
case database.WorkspaceTransitionStart:
builder = builder.Reason(database.BuildReasonAutostart)
case database.WorkspaceTransitionStop:
builder = builder.Reason(database.BuildReasonAutostop)
default:
log.Error(e.ctx, "unsupported transition", slog.F("transition", validTransition))
return nil
}
if _, _, err := builder.Build(e.ctx, db, nil); err != nil {
log.Error(e.ctx, "unable to transition workspace",
slog.F("transition", validTransition),
slog.Error(err),
)
return nil
}
stats.Transitions[ws.ID] = validTransition
log.Info(e.ctx, "scheduling workspace transition", slog.F("transition", validTransition))
return nil
}, nil)
// Run with RepeatableRead isolation so that the build process sees the same data
// as our calculation that determines whether an autobuild is necessary.
}, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
if err != nil {
log.Error(e.ctx, "workspace scheduling failed", slog.Error(err))
}
@ -248,92 +263,3 @@ func getNextTransition(
return "", time.Time{}, xerrors.Errorf("last transition not valid for autostart or autostop")
}
}
// TODO(cian): this function duplicates most of api.postWorkspaceBuilds. Refactor.
// See: https://github.com/coder/coder/issues/1401
func build(ctx context.Context, store database.Store, workspace database.Workspace, trans database.WorkspaceTransition, priorHistory database.WorkspaceBuild, priorJob database.ProvisionerJob) error {
template, err := store.GetTemplateByID(ctx, workspace.TemplateID)
if err != nil {
return xerrors.Errorf("get workspace template: %w", err)
}
priorBuildNumber := priorHistory.BuildNumber
// This must happen in a transaction to ensure history can be inserted, and
// the prior history can update it's "after" column to point at the new.
workspaceBuildID := uuid.New()
input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{
WorkspaceBuildID: workspaceBuildID,
})
if err != nil {
return xerrors.Errorf("marshal provision job: %w", err)
}
provisionerJobID := uuid.New()
now := database.Now()
var buildReason database.BuildReason
switch trans {
case database.WorkspaceTransitionStart:
buildReason = database.BuildReasonAutostart
case database.WorkspaceTransitionStop:
buildReason = database.BuildReasonAutostop
default:
return xerrors.Errorf("Unsupported transition: %q", trans)
}
lastBuildParameters, err := store.GetWorkspaceBuildParameters(ctx, priorHistory.ID)
if err != nil {
return xerrors.Errorf("fetch prior workspace build parameters: %w", err)
}
return store.InTx(func(db database.Store) error {
newProvisionerJob, err := store.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
ID: provisionerJobID,
CreatedAt: now,
UpdatedAt: now,
InitiatorID: workspace.OwnerID,
OrganizationID: template.OrganizationID,
Provisioner: template.Provisioner,
Type: database.ProvisionerJobTypeWorkspaceBuild,
StorageMethod: priorJob.StorageMethod,
FileID: priorJob.FileID,
Tags: priorJob.Tags,
Input: input,
})
if err != nil {
return xerrors.Errorf("insert provisioner job: %w", err)
}
workspaceBuild, err := store.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
ID: workspaceBuildID,
CreatedAt: now,
UpdatedAt: now,
WorkspaceID: workspace.ID,
TemplateVersionID: priorHistory.TemplateVersionID,
BuildNumber: priorBuildNumber + 1,
ProvisionerState: priorHistory.ProvisionerState,
InitiatorID: workspace.OwnerID,
Transition: trans,
JobID: newProvisionerJob.ID,
Reason: buildReason,
})
if err != nil {
return xerrors.Errorf("insert workspace build: %w", err)
}
names := make([]string, 0, len(lastBuildParameters))
values := make([]string, 0, len(lastBuildParameters))
for _, param := range lastBuildParameters {
names = append(names, param.Name)
values = append(values, param.Value)
}
err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{
WorkspaceBuildID: workspaceBuild.ID,
Name: names,
Value: values,
})
if err != nil {
return xerrors.Errorf("insert workspace build parameters: %w", err)
}
return nil
}, nil)
}

View File

@ -0,0 +1,113 @@
// Package db2sdk provides common conversion routines from database types to codersdk types
package db2sdk
import (
"encoding/json"
"time"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/parameter"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisionersdk/proto"
)
func WorkspaceBuildParameters(params []database.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter {
out := make([]codersdk.WorkspaceBuildParameter, len(params))
for i, p := range params {
out[i] = WorkspaceBuildParameter(p)
}
return out
}
func WorkspaceBuildParameter(p database.WorkspaceBuildParameter) codersdk.WorkspaceBuildParameter {
return codersdk.WorkspaceBuildParameter{
Name: p.Name,
Value: p.Value,
}
}
func TemplateVersionParameter(param database.TemplateVersionParameter) (codersdk.TemplateVersionParameter, error) {
var protoOptions []*proto.RichParameterOption
err := json.Unmarshal(param.Options, &protoOptions)
if err != nil {
return codersdk.TemplateVersionParameter{}, err
}
options := make([]codersdk.TemplateVersionParameterOption, 0)
for _, option := range protoOptions {
options = append(options, codersdk.TemplateVersionParameterOption{
Name: option.Name,
Description: option.Description,
Value: option.Value,
Icon: option.Icon,
})
}
descriptionPlaintext, err := parameter.Plaintext(param.Description)
if err != nil {
return codersdk.TemplateVersionParameter{}, err
}
return codersdk.TemplateVersionParameter{
Name: param.Name,
DisplayName: param.DisplayName,
Description: param.Description,
DescriptionPlaintext: descriptionPlaintext,
Type: param.Type,
Mutable: param.Mutable,
DefaultValue: param.DefaultValue,
Icon: param.Icon,
Options: options,
ValidationRegex: param.ValidationRegex,
ValidationMin: param.ValidationMin,
ValidationMax: param.ValidationMax,
ValidationError: param.ValidationError,
ValidationMonotonic: codersdk.ValidationMonotonicOrder(param.ValidationMonotonic),
Required: param.Required,
LegacyVariableName: param.LegacyVariableName,
}, nil
}
func Parameters(params []database.ParameterValue) []codersdk.Parameter {
out := make([]codersdk.Parameter, len(params))
for i, p := range params {
out[i] = Parameter(p)
}
return out
}
func Parameter(parameterValue database.ParameterValue) codersdk.Parameter {
return codersdk.Parameter{
ID: parameterValue.ID,
CreatedAt: parameterValue.CreatedAt,
UpdatedAt: parameterValue.UpdatedAt,
Scope: codersdk.ParameterScope(parameterValue.Scope),
ScopeID: parameterValue.ScopeID,
Name: parameterValue.Name,
SourceScheme: codersdk.ParameterSourceScheme(parameterValue.SourceScheme),
DestinationScheme: codersdk.ParameterDestinationScheme(parameterValue.DestinationScheme),
SourceValue: parameterValue.SourceValue,
}
}
func ProvisionerJobStatus(provisionerJob database.ProvisionerJob) codersdk.ProvisionerJobStatus {
switch {
case provisionerJob.CanceledAt.Valid:
if !provisionerJob.CompletedAt.Valid {
return codersdk.ProvisionerJobCanceling
}
if provisionerJob.Error.String == "" {
return codersdk.ProvisionerJobCanceled
}
return codersdk.ProvisionerJobFailed
case !provisionerJob.StartedAt.Valid:
return codersdk.ProvisionerJobPending
case provisionerJob.CompletedAt.Valid:
if provisionerJob.Error.String == "" {
return codersdk.ProvisionerJobSucceeded
}
return codersdk.ProvisionerJobFailed
case database.Now().Sub(provisionerJob.UpdatedAt) > 30*time.Second:
return codersdk.ProvisionerJobFailed
default:
return codersdk.ProvisionerJobRunning
}
}

View File

@ -0,0 +1,191 @@
package db2sdk_test
import (
"crypto/rand"
"database/sql"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/db2sdk"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisionersdk/proto"
)
func TestProvisionerJobStatus(t *testing.T) {
t.Parallel()
cases := []struct {
name string
job database.ProvisionerJob
status codersdk.ProvisionerJobStatus
}{
{
name: "canceling",
job: database.ProvisionerJob{
CanceledAt: sql.NullTime{
Time: database.Now().Add(-time.Minute),
Valid: true,
},
},
status: codersdk.ProvisionerJobCanceling,
},
{
name: "canceled",
job: database.ProvisionerJob{
CanceledAt: sql.NullTime{
Time: database.Now().Add(-time.Minute),
Valid: true,
},
CompletedAt: sql.NullTime{
Time: database.Now().Add(-30 * time.Second),
Valid: true,
},
},
status: codersdk.ProvisionerJobCanceled,
},
{
name: "canceled_failed",
job: database.ProvisionerJob{
CanceledAt: sql.NullTime{
Time: database.Now().Add(-time.Minute),
Valid: true,
},
CompletedAt: sql.NullTime{
Time: database.Now().Add(-30 * time.Second),
Valid: true,
},
Error: sql.NullString{String: "badness", Valid: true},
},
status: codersdk.ProvisionerJobFailed,
},
{
name: "pending",
job: database.ProvisionerJob{},
status: codersdk.ProvisionerJobPending,
},
{
name: "succeeded",
job: database.ProvisionerJob{
StartedAt: sql.NullTime{
Time: database.Now().Add(-time.Minute),
Valid: true,
},
CompletedAt: sql.NullTime{
Time: database.Now().Add(-30 * time.Second),
Valid: true,
},
},
status: codersdk.ProvisionerJobSucceeded,
},
{
name: "completed_failed",
job: database.ProvisionerJob{
StartedAt: sql.NullTime{
Time: database.Now().Add(-time.Minute),
Valid: true,
},
CompletedAt: sql.NullTime{
Time: database.Now().Add(-30 * time.Second),
Valid: true,
},
Error: sql.NullString{String: "badness", Valid: true},
},
status: codersdk.ProvisionerJobFailed,
},
{
name: "not_updated",
job: database.ProvisionerJob{
StartedAt: sql.NullTime{
Time: database.Now().Add(-time.Minute),
Valid: true,
},
UpdatedAt: database.Now().Add(-31 * time.Second),
},
status: codersdk.ProvisionerJobFailed,
},
{
name: "updated",
job: database.ProvisionerJob{
StartedAt: sql.NullTime{
Time: database.Now().Add(-time.Minute),
Valid: true,
},
UpdatedAt: database.Now(),
},
status: codersdk.ProvisionerJobRunning,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
actual := db2sdk.ProvisionerJobStatus(tc.job)
require.Equal(t, tc.status, actual)
})
}
}
func TestTemplateVersionParameter_OK(t *testing.T) {
t.Parallel()
req := require.New(t)
// In this test we're just going to cover the fields that have to get parsed.
options := []*proto.RichParameterOption{
{
Name: "foo",
Description: "bar",
Value: "baz",
Icon: "David Bowie",
},
}
ob, err := json.Marshal(&options)
req.NoError(err)
db := database.TemplateVersionParameter{
Options: json.RawMessage(ob),
Description: "_The Rise and Fall of **Ziggy Stardust** and the Spiders from Mars_",
}
sdk, err := db2sdk.TemplateVersionParameter(db)
req.NoError(err)
req.Len(sdk.Options, 1)
req.Equal("foo", sdk.Options[0].Name)
req.Equal("bar", sdk.Options[0].Description)
req.Equal("baz", sdk.Options[0].Value)
req.Equal("David Bowie", sdk.Options[0].Icon)
req.Equal("The Rise and Fall of Ziggy Stardust and the Spiders from Mars", sdk.DescriptionPlaintext)
}
func TestTemplateVersionParameter_BadOptions(t *testing.T) {
t.Parallel()
req := require.New(t)
db := database.TemplateVersionParameter{
Options: json.RawMessage("not really JSON!"),
Description: "_The Rise and Fall of **Ziggy Stardust** and the Spiders from Mars_",
}
_, err := db2sdk.TemplateVersionParameter(db)
req.Error(err)
}
func TestTemplateVersionParameter_BadDescription(t *testing.T) {
t.Parallel()
req := require.New(t)
desc := make([]byte, 300)
_, err := rand.Read(desc)
req.NoError(err)
db := database.TemplateVersionParameter{
Options: json.RawMessage("[]"),
Description: string(desc),
}
sdk, err := db2sdk.TemplateVersionParameter(db)
// Although the markdown parser can return an error, the way we use it should not, even
// if we feed it garbage data.
req.NoError(err)
req.NotEmpty(sdk.DescriptionPlaintext, "broke the markdown parser with %v", desc)
}

View File

@ -0,0 +1,4 @@
// package dbmock contains a mocked implementation of the database.Store interface for use in tests
package dbmock
//go:generate mockgen -destination ./store.go -package dbmock github.com/coder/coder/coderd/database Store

File diff suppressed because it is too large Load Diff

View File

@ -96,12 +96,12 @@ func Is404Error(err error) bool {
// Convenience error functions don't take contexts since their responses are
// static, it doesn't make much sense to trace them.
var ResourceNotFoundResponse = codersdk.Response{Message: "Resource not found or you do not have access to this resource"}
// ResourceNotFound is intentionally vague. All 404 responses should be identical
// to prevent leaking existence of resources.
func ResourceNotFound(rw http.ResponseWriter) {
Write(context.Background(), rw, http.StatusNotFound, codersdk.Response{
Message: "Resource not found or you do not have access to this resource",
})
Write(context.Background(), rw, http.StatusNotFound, ResourceNotFoundResponse)
}
func Forbidden(rw http.ResponseWriter) {

View File

@ -206,6 +206,7 @@ func convertParameterValue(parameterValue database.ParameterValue) codersdk.Para
Name: parameterValue.Name,
SourceScheme: codersdk.ParameterSourceScheme(parameterValue.SourceScheme),
DestinationScheme: codersdk.ParameterDestinationScheme(parameterValue.DestinationScheme),
SourceValue: parameterValue.SourceValue,
}
}

View File

@ -16,8 +16,8 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/db2sdk"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/tailnet"
)
@ -124,7 +124,7 @@ func Workspaces(ctx context.Context, registerer prometheus.Registerer, db databa
gauge.Reset()
for _, job := range jobs {
status := coderd.ConvertProvisionerJobStatus(job)
status := db2sdk.ProvisionerJobStatus(job)
gauge.WithLabelValues(string(status)).Add(1)
}
}

View File

@ -9,7 +9,6 @@ import (
"net/http"
"sort"
"strconv"
"time"
"github.com/google/uuid"
"go.uber.org/atomic"
@ -17,6 +16,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/db2sdk"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/rbac"
@ -329,36 +329,11 @@ func convertProvisionerJob(provisionerJob database.ProvisionerJob) codersdk.Prov
if provisionerJob.WorkerID.Valid {
job.WorkerID = &provisionerJob.WorkerID.UUID
}
job.Status = ConvertProvisionerJobStatus(provisionerJob)
job.Status = db2sdk.ProvisionerJobStatus(provisionerJob)
return job
}
func ConvertProvisionerJobStatus(provisionerJob database.ProvisionerJob) codersdk.ProvisionerJobStatus {
switch {
case provisionerJob.CanceledAt.Valid:
if !provisionerJob.CompletedAt.Valid {
return codersdk.ProvisionerJobCanceling
}
if provisionerJob.Error.String == "" {
return codersdk.ProvisionerJobCanceled
}
return codersdk.ProvisionerJobFailed
case !provisionerJob.StartedAt.Valid:
return codersdk.ProvisionerJobPending
case provisionerJob.CompletedAt.Valid:
if provisionerJob.Error.String == "" {
return codersdk.ProvisionerJobSucceeded
}
return codersdk.ProvisionerJobFailed
case database.Now().Sub(provisionerJob.UpdatedAt) > 30*time.Second:
provisionerJob.Error.String = "Worker failed to update job in time."
return codersdk.ProvisionerJobFailed
default:
return codersdk.ProvisionerJobRunning
}
}
func provisionerJobLogsChannel(jobID uuid.UUID) string {
return fmt.Sprintf("provisioner-log-logs:%s", jobID)
}

View File

@ -203,6 +203,15 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
return
}
templateVersionAudit.Old = templateVersion
if templateVersion.TemplateID.Valid {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Template version %s is already part of a template", createTemplate.VersionID),
Validations: []codersdk.ValidationError{
{Field: "template_version_id", Detail: "Template version is already part of a template"},
},
})
return
}
importJob, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID)
if err != nil {

View File

@ -347,8 +347,9 @@ func TestTemplatesByOrganization(t *testing.T) {
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

View File

@ -3,7 +3,6 @@ package coderd
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
@ -12,17 +11,16 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/tabbed/pqtype"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/db2sdk"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/coderd/wsbuilder"
"github.com/coder/coder/codersdk"
)
@ -313,363 +311,53 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
return
}
// 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 createBuild.Transition {
case codersdk.WorkspaceTransitionDelete:
action = rbac.ActionDelete
case codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop:
action = rbac.ActionUpdate
default:
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: fmt.Sprintf("Transition %q not supported.", createBuild.Transition),
})
return
}
if !api.Authorize(r, action, workspace) {
httpapi.ResourceNotFound(rw)
return
}
builder := wsbuilder.New(workspace, database.WorkspaceTransition(createBuild.Transition)).
Initiator(apiKey.UserID).
LegacyParameterValues(createBuild.ParameterValues).
RichParameterValues(createBuild.RichParameterValues).
LogLevel(string(createBuild.LogLevel))
if createBuild.TemplateVersionID == uuid.Nil {
latestBuild, latestBuildErr := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
if latestBuildErr != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching the latest workspace build.",
Detail: latestBuildErr.Error(),
})
return
}
createBuild.TemplateVersionID = latestBuild.TemplateVersionID
}
templateVersion, err := api.Database.GetTemplateVersionByID(ctx, createBuild.TemplateVersionID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Template version not found.",
Validations: []codersdk.ValidationError{{
Field: "template_version_id",
Detail: "template version not found",
}},
})
return
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template version.",
Detail: err.Error(),
})
return
}
template, err := api.Database.GetTemplateByID(ctx, templateVersion.TemplateID.UUID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to get template",
Detail: err.Error(),
})
return
}
var state []byte
// If custom state, deny request since user could be corrupting or leaking
// cloud state.
if createBuild.ProvisionerState != nil || createBuild.Orphan {
if !api.Authorize(r, rbac.ActionUpdate, template.RBACObject()) {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: "Only template managers may provide custom state",
})
return
}
state = createBuild.ProvisionerState
if createBuild.TemplateVersionID != uuid.Nil {
builder = builder.VersionID(createBuild.TemplateVersionID)
}
if createBuild.Orphan {
if createBuild.Transition != codersdk.WorkspaceTransitionDelete {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Orphan is only permitted when deleting a workspace.",
Detail: err.Error(),
})
return
}
if createBuild.ProvisionerState != nil && createBuild.Orphan {
if len(createBuild.ProvisionerState) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.",
})
return
}
state = []byte{}
builder = builder.Orphan()
}
if len(createBuild.ProvisionerState) > 0 {
builder = builder.State(createBuild.ProvisionerState)
}
templateVersionJob, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID)
workspaceBuild, provisionerJob, err := builder.Build(
ctx,
api.Database,
func(action rbac.Action, object rbac.Objecter) bool {
return api.Authorize(r, action, object)
},
)
var buildErr wsbuilder.BuildError
if xerrors.As(err, &buildErr) {
httpapi.Write(ctx, rw, buildErr.Status, codersdk.Response{
Message: buildErr.Message,
Detail: buildErr.Error(),
})
return
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner job.",
Detail: err.Error(),
})
return
}
templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status
switch templateVersionJobStatus {
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
httpapi.Write(ctx, rw, http.StatusNotAcceptable, codersdk.Response{
Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus),
})
return
case codersdk.ProvisionerJobFailed:
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String),
})
return
case codersdk.ProvisionerJobCanceled:
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "The provided template version was canceled during import. You cannot builds workspaces with it!",
})
return
}
tags := provisionerdserver.MutateTags(workspace.OwnerID, templateVersionJob.Tags)
// Store prior build number to compute new build number
var priorBuildNum int32
priorHistory, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
if err == nil {
priorJob, err := api.Database.GetProvisionerJobByID(ctx, priorHistory.JobID)
if err == nil && convertProvisionerJob(priorJob).Status.Active() {
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
Message: "A workspace build is already active.",
})
return
}
priorBuildNum = priorHistory.BuildNumber
} else if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching prior workspace build.",
Detail: err.Error(),
})
return
}
if state == nil {
state = priorHistory.ProvisionerState
}
dbTemplateVersionParameters, err := api.Database.GetTemplateVersionParameters(ctx, createBuild.TemplateVersionID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template version parameters.",
Detail: err.Error(),
})
return
}
templateVersionParameters, err := convertTemplateVersionParameters(dbTemplateVersionParameters)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting template version parameters.",
Detail: err.Error(),
})
return
}
lastBuildParameters, err := api.Database.GetWorkspaceBuildParameters(ctx, priorHistory.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching prior workspace build parameters.",
Detail: err.Error(),
})
return
}
apiLastBuildParameters := convertWorkspaceBuildParameters(lastBuildParameters)
legacyParameters, err := api.Database.ParameterValues(ctx, database.ParameterValuesParams{
Scopes: []database.ParameterScope{database.ParameterScopeWorkspace},
ScopeIds: []uuid.UUID{workspace.ID},
})
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Error fetching previous legacy parameters.",
Detail: err.Error(),
})
return
}
// Rich parameters migration: include legacy variables to the last build parameters
for _, templateVersionParameter := range templateVersionParameters {
// Check if parameter is defined in previous build
if _, found := findWorkspaceBuildParameter(apiLastBuildParameters, templateVersionParameter.Name); found {
continue
}
// Check if legacy variable is defined
for _, legacyParameter := range legacyParameters {
if legacyParameter.Name != templateVersionParameter.LegacyVariableName {
continue
}
apiLastBuildParameters = append(apiLastBuildParameters, codersdk.WorkspaceBuildParameter{
Name: templateVersionParameter.Name,
Value: legacyParameter.SourceValue,
})
break
}
}
err = codersdk.ValidateWorkspaceBuildParameters(templateVersionParameters, createBuild.RichParameterValues, apiLastBuildParameters)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Error validating workspace build parameters.",
Detail: err.Error(),
})
return
}
var parameters []codersdk.WorkspaceBuildParameter
for _, templateVersionParameter := range templateVersionParameters {
// Check if parameter value is in request
if buildParameter, found := findWorkspaceBuildParameter(createBuild.RichParameterValues, templateVersionParameter.Name); found {
if !templateVersionParameter.Mutable {
if _, found := findWorkspaceBuildParameter(apiLastBuildParameters, templateVersionParameter.Name); found {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Parameter %q is not mutable, so it can't be updated after creating a workspace.", templateVersionParameter.Name),
})
return
}
}
parameters = append(parameters, *buildParameter)
continue
}
// Check if parameter is defined in previous build
if buildParameter, found := findWorkspaceBuildParameter(apiLastBuildParameters, templateVersionParameter.Name); found {
parameters = append(parameters, *buildParameter)
continue
}
// Check if default parameter value is in schema
if templateVersionParameter.DefaultValue != "" {
parameters = append(parameters, codersdk.WorkspaceBuildParameter{
Name: templateVersionParameter.Name,
Value: templateVersionParameter.DefaultValue,
})
}
}
if createBuild.LogLevel != "" && !api.Authorize(r, rbac.ActionUpdate, template) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Workspace builds with a custom log level are restricted to template authors only.",
})
return
}
var workspaceBuild database.WorkspaceBuild
var provisionerJob database.ProvisionerJob
// This must happen in a transaction to ensure history can be inserted, and
// the prior history can update it's "after" column to point at the new.
err = api.Database.InTx(func(db database.Store) error {
// Write/Update any new params
now := database.Now()
for _, param := range createBuild.ParameterValues {
for _, exists := range legacyParameters {
// If the param exists, delete the old param before inserting the new one
if exists.Name == param.Name {
err = db.DeleteParameterValueByID(ctx, exists.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return xerrors.Errorf("Failed to delete old param %q: %w", exists.Name, err)
}
}
}
_, err = db.InsertParameterValue(ctx, database.InsertParameterValueParams{
ID: uuid.New(),
Name: param.Name,
CreatedAt: now,
UpdatedAt: now,
Scope: database.ParameterScopeWorkspace,
ScopeID: workspace.ID,
SourceScheme: database.ParameterSourceScheme(param.SourceScheme),
SourceValue: param.SourceValue,
DestinationScheme: database.ParameterDestinationScheme(param.DestinationScheme),
})
if err != nil {
return xerrors.Errorf("insert parameter value: %w", err)
}
}
workspaceBuildID := uuid.New()
input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{
WorkspaceBuildID: workspaceBuildID,
LogLevel: string(createBuild.LogLevel),
})
if err != nil {
return xerrors.Errorf("marshal provision job: %w", err)
}
traceMetadataRaw, err := json.Marshal(tracing.MetadataFromContext(ctx))
if err != nil {
return xerrors.Errorf("marshal metadata: %w", err)
}
provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
InitiatorID: apiKey.UserID,
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 xerrors.Errorf("insert provisioner job: %w", err)
}
workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
ID: workspaceBuildID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
WorkspaceID: workspace.ID,
TemplateVersionID: templateVersion.ID,
BuildNumber: priorBuildNum + 1,
ProvisionerState: state,
InitiatorID: apiKey.UserID,
Transition: database.WorkspaceTransition(createBuild.Transition),
JobID: provisionerJob.ID,
Reason: database.BuildReasonInitiator,
})
if err != nil {
return xerrors.Errorf("insert workspace build: %w", err)
}
names := make([]string, 0, len(parameters))
values := make([]string, 0, len(parameters))
for _, param := range parameters {
names = append(names, param.Name)
values = append(values, param.Value)
}
err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{
WorkspaceBuildID: workspaceBuildID,
Name: names,
Value: values,
})
if err != nil {
return xerrors.Errorf("insert workspace build parameters: %w", err)
}
return nil
}, nil)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error inserting workspace build.",
Message: "Error posting new build",
Detail: err.Error(),
})
return
@ -688,9 +376,9 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
}
apiBuild, err := api.convertWorkspaceBuild(
workspaceBuild,
*workspaceBuild,
workspace,
provisionerJob,
*provisionerJob,
users,
[]database.WorkspaceResource{},
[]database.WorkspaceResourceMetadatum{},
@ -852,7 +540,7 @@ func (api *API) workspaceBuildParameters(rw http.ResponseWriter, r *http.Request
})
return
}
apiParameters := convertWorkspaceBuildParameters(parameters)
apiParameters := db2sdk.WorkspaceBuildParameters(parameters)
httpapi.Write(ctx, rw, http.StatusOK, apiParameters)
}
@ -1232,25 +920,3 @@ func convertWorkspaceStatus(jobStatus codersdk.ProvisionerJobStatus, transition
// return error status since we should never get here
return codersdk.WorkspaceStatusFailed
}
func convertWorkspaceBuildParameters(parameters []database.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter {
apiParameters := make([]codersdk.WorkspaceBuildParameter, 0, len(parameters))
for _, p := range parameters {
apiParameter := codersdk.WorkspaceBuildParameter{
Name: p.Name,
Value: p.Value,
}
apiParameters = append(apiParameters, apiParameter)
}
return apiParameters
}
func findWorkspaceBuildParameter(params []codersdk.WorkspaceBuildParameter, parameterName string) (*codersdk.WorkspaceBuildParameter, bool) {
for _, p := range params {
if p.Name == parameterName {
return &p, true
}
}
return nil, false
}

View File

@ -637,565 +637,6 @@ func TestWorkspaceBuildStatus(t *testing.T) {
require.EqualValues(t, codersdk.WorkspaceStatusDeleted, workspace.LatestBuild.Status)
}
func TestWorkspaceBuildWithRichParameters(t *testing.T) {
t.Parallel()
const (
firstParameterName = "first_parameter"
firstParameterDescription = "This is first parameter"
firstParameterValue = "1"
secondParameterName = "second_parameter"
secondParameterDescription = "This is second parameter"
secondParameterValue = "2"
immutableParameterName = "immutable_parameter"
immutableParameterDescription = "This is immutable parameter"
immutableParameterValue = "3"
)
initialBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: firstParameterName, Value: firstParameterValue},
{Name: secondParameterName, Value: secondParameterValue},
{Name: immutableParameterName, Value: immutableParameterValue},
}
echoResponses := &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: []*proto.RichParameter{
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true},
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
},
},
},
},
},
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
}
t.Run("UpdateParameterValues", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
// Update build parameters
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
const updatedParameterValue = "3"
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: firstParameterName, Value: firstParameterValue},
{Name: secondParameterName, Value: updatedParameterValue},
}
nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: nextBuildParameters,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID)
workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, nextWorkspaceBuild.ID)
require.NoError(t, err)
expected := append(nextBuildParameters, codersdk.WorkspaceBuildParameter{
Name: immutableParameterName,
Value: immutableParameterValue,
})
require.ElementsMatch(t, expected, workspaceBuildParameters)
})
t.Run("UsePreviousParameterValues", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
firstWorkspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, firstWorkspaceBuild.Status)
// Start new workspace build
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
})
require.NoError(t, err)
require.NotEqual(t, firstWorkspaceBuild, nextWorkspaceBuild)
coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID)
workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, nextWorkspaceBuild.ID)
require.NoError(t, err)
require.ElementsMatch(t, initialBuildParameters, workspaceBuildParameters)
})
t.Run("DoNotModifyImmutables", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
// Update build parameters
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: immutableParameterName, Value: "BAD"},
}
_, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: nextBuildParameters,
})
require.Error(t, err)
})
t.Run("NewImmutableRequiredParameterAdded", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
// Push new template revision
const newImmutableParameterName = "new_immutable_parameter"
const newImmutableParameterDescription = "This is also an immutable parameter"
version2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: []*proto.RichParameter{
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true},
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
{Name: newImmutableParameterName, Description: newImmutableParameterDescription, Mutable: false, Required: true},
},
},
},
},
},
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
}, template.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version2.ID)
err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
ID: version2.ID,
})
require.NoError(t, err)
// Update build parameters
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: newImmutableParameterName, Value: "good"},
}
_, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: version2.ID,
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: nextBuildParameters,
})
require.NoError(t, err)
})
t.Run("NewImmutableOptionalParameterAdded", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
// Push new template revision
const newImmutableParameterName = "new_immutable_parameter"
const newImmutableParameterDescription = "This is also an immutable parameter"
version2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: []*proto.RichParameter{
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true},
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
{Name: newImmutableParameterName, Description: newImmutableParameterDescription, Mutable: false, DefaultValue: "12345"},
},
},
},
},
},
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
}, template.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version2.ID)
err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
ID: version2.ID,
})
require.NoError(t, err)
// Update build parameters
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: newImmutableParameterName, Value: "good"},
}
nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: version2.ID,
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: nextBuildParameters,
})
require.NoError(t, err)
require.NotEqual(t, workspaceBuild, nextWorkspaceBuild)
coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID)
workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, nextWorkspaceBuild.ID)
require.NoError(t, err)
expectedNextBuildParameters := append(initialBuildParameters, codersdk.WorkspaceBuildParameter{
Name: newImmutableParameterName,
Value: "good",
})
require.ElementsMatch(t, expectedNextBuildParameters, workspaceBuildParameters)
})
t.Run("NewImmutableOptionalParameterUsesDefault", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
// Push new template revision
const newImmutableParameterName = "new_immutable_parameter"
const newImmutableParameterDescription = "This is also an immutable parameter"
version2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: []*proto.RichParameter{
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true},
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
{Name: newImmutableParameterName, Description: newImmutableParameterDescription, Mutable: false, DefaultValue: "12345"},
},
},
},
},
},
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
}, template.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version2.ID)
err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
ID: version2.ID,
})
require.NoError(t, err)
// Update build parameters
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
var nextBuildParameters []codersdk.WorkspaceBuildParameter
nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: version2.ID,
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: nextBuildParameters,
})
require.NoError(t, err)
require.NotEqual(t, workspaceBuild, nextWorkspaceBuild)
coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID)
workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, nextWorkspaceBuild.ID)
require.NoError(t, err)
expectedNextBuildParameters := append(initialBuildParameters, codersdk.WorkspaceBuildParameter{
Name: newImmutableParameterName,
Value: "12345",
})
require.ElementsMatch(t, expectedNextBuildParameters, workspaceBuildParameters)
})
}
func TestWorkspaceBuildValidateRichParameters(t *testing.T) {
t.Parallel()
const (
stringParameterName = "string_parameter"
stringParameterValue = "abc"
numberParameterName = "number_parameter"
numberParameterValue = "7"
boolParameterName = "bool_parameter"
boolParameterValue = "true"
listOfStringsParameterName = "list_of_strings_parameter"
listOfStringsParameterValue = `["a","b","c"]`
)
initialBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: stringParameterName, Value: stringParameterValue},
{Name: numberParameterName, Value: numberParameterValue},
{Name: boolParameterName, Value: boolParameterValue},
{Name: listOfStringsParameterName, Value: listOfStringsParameterValue},
}
prepareEchoResponses := func(richParameters []*proto.RichParameter) *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: richParameters,
},
},
},
},
ProvisionApply: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
},
},
}
}
t.Run("NoValidation", func(t *testing.T) {
t.Parallel()
richParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(richParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
// Update build parameters
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: numberParameterName, Value: "42"},
}
nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: nextBuildParameters,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID)
_, err = client.WorkspaceBuildParameters(ctx, nextWorkspaceBuild.ID)
require.NoError(t, err)
})
t.Run("Validation", func(t *testing.T) {
t.Parallel()
numberRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
monotonicIncreasingNumberRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10, ValidationMonotonic: "increasing"},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
monotonicDecreasingNumberRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10, ValidationMonotonic: "decreasing"},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
stringRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
boolRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
regexRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true, ValidationRegex: "^[a-z]+$", ValidationError: "this is error"},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
listOfStringsRichParameters := []*proto.RichParameter{
{Name: listOfStringsParameterName, Type: "list(string)", Mutable: true},
}
tests := []struct {
parameterName string
value string
valid bool
richParameters []*proto.RichParameter
}{
{numberParameterName, "2", false, numberRichParameters},
{numberParameterName, "3", true, numberRichParameters},
{numberParameterName, "10", true, numberRichParameters},
{numberParameterName, "11", false, numberRichParameters},
{numberParameterName, "6", false, monotonicIncreasingNumberRichParameters},
{numberParameterName, "7", true, monotonicIncreasingNumberRichParameters},
{numberParameterName, "8", true, monotonicIncreasingNumberRichParameters},
{numberParameterName, "6", true, monotonicDecreasingNumberRichParameters},
{numberParameterName, "7", true, monotonicDecreasingNumberRichParameters},
{numberParameterName, "8", false, monotonicDecreasingNumberRichParameters},
{stringParameterName, "", true, stringRichParameters},
{stringParameterName, "foobar", true, stringRichParameters},
{stringParameterName, "abcd", true, regexRichParameters},
{stringParameterName, "abcd1", false, regexRichParameters},
{boolParameterName, "true", true, boolRichParameters},
{boolParameterName, "false", true, boolRichParameters},
{boolParameterName, "cat", false, boolRichParameters},
{listOfStringsParameterName, `[]`, true, listOfStringsRichParameters},
{listOfStringsParameterName, `["aa"]`, true, listOfStringsRichParameters},
{listOfStringsParameterName, `["aa]`, false, listOfStringsRichParameters},
{listOfStringsParameterName, ``, false, listOfStringsRichParameters},
}
for _, tc := range tests {
tc := tc
t.Run(tc.parameterName+"-"+tc.value, func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(tc.richParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: tc.parameterName, Value: tc.value},
}
nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: nextBuildParameters,
})
if tc.valid {
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID)
} else {
require.Error(t, err)
}
})
}
})
}
func TestMigrateLegacyToRichParameters(t *testing.T) {
t.Parallel()

View File

@ -13,7 +13,6 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/tabbed/pqtype"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
@ -22,13 +21,12 @@ import (
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/searchquery"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/coderd/wsbuilder"
"github.com/coder/coder/codersdk"
)
@ -399,77 +397,12 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
return
}
templateVersion, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template version.",
Detail: err.Error(),
})
return
}
dbTemplateVersionParameters, err := api.Database.GetTemplateVersionParameters(ctx, templateVersion.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template version parameters.",
Detail: err.Error(),
})
return
}
templateVersionParameters, err := convertTemplateVersionParameters(dbTemplateVersionParameters)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting template version parameters.",
Detail: err.Error(),
})
return
}
err = codersdk.ValidateNewWorkspaceParameters(templateVersionParameters, createWorkspace.RichParameterValues)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Error validating workspace build parameters.",
Detail: err.Error(),
})
return
}
templateVersionJob, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template version job.",
Detail: err.Error(),
})
return
}
templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status
switch templateVersionJobStatus {
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
httpapi.Write(ctx, rw, http.StatusNotAcceptable, codersdk.Response{
Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus),
})
return
case codersdk.ProvisionerJobFailed:
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("The provided template version %q has failed to import. You cannot create workspaces using it!", templateVersion.Name),
})
return
case codersdk.ProvisionerJobCanceled:
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "The provided template version was canceled during import. You cannot create workspaces using it!",
})
return
}
tags := provisionerdserver.MutateTags(user.ID, templateVersionJob.Tags)
var (
provisionerJob database.ProvisionerJob
workspaceBuild database.WorkspaceBuild
provisionerJob *database.ProvisionerJob
workspaceBuild *database.WorkspaceBuild
)
err = api.Database.InTx(func(db database.Store) error {
now := database.Now()
workspaceBuildID := uuid.New()
// Workspaces are created without any versions.
workspace, err = db.InsertWorkspace(ctx, database.InsertWorkspaceParams{
ID: uuid.New(),
@ -488,92 +421,27 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
if err != nil {
return xerrors.Errorf("insert workspace: %w", err)
}
for _, parameterValue := range createWorkspace.ParameterValues {
// If the value is empty, we don't want to save it on database so
// Terraform can use the default value
if parameterValue.SourceValue == "" {
continue
}
_, err = db.InsertParameterValue(ctx, database.InsertParameterValueParams{
ID: uuid.New(),
Name: parameterValue.Name,
CreatedAt: now,
UpdatedAt: now,
Scope: database.ParameterScopeWorkspace,
ScopeID: workspace.ID,
SourceScheme: database.ParameterSourceScheme(parameterValue.SourceScheme),
SourceValue: parameterValue.SourceValue,
DestinationScheme: database.ParameterDestinationScheme(parameterValue.DestinationScheme),
builder := wsbuilder.New(workspace, database.WorkspaceTransitionStart).
Reason(database.BuildReasonInitiator).
Initiator(apiKey.UserID).
ActiveVersion().
LegacyParameterValues(createWorkspace.ParameterValues).
RichParameterValues(createWorkspace.RichParameterValues)
workspaceBuild, provisionerJob, err = builder.Build(
ctx, db, func(action rbac.Action, object rbac.Objecter) bool {
return api.Authorize(r, action, object)
})
if err != nil {
return xerrors.Errorf("insert parameter value: %w", err)
}
}
input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{
WorkspaceBuildID: workspaceBuildID,
})
if err != nil {
return xerrors.Errorf("marshal provision job: %w", err)
}
traceMetadataRaw, err := json.Marshal(tracing.MetadataFromContext(ctx))
if err != nil {
return xerrors.Errorf("marshal metadata: %w", err)
}
provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
ID: uuid.New(),
CreatedAt: now,
UpdatedAt: now,
InitiatorID: apiKey.UserID,
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 xerrors.Errorf("insert provisioner job: %w", err)
}
workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
ID: workspaceBuildID,
CreatedAt: now,
UpdatedAt: now,
WorkspaceID: workspace.ID,
TemplateVersionID: templateVersion.ID,
InitiatorID: apiKey.UserID,
Transition: database.WorkspaceTransitionStart,
JobID: provisionerJob.ID,
BuildNumber: 1, // First build!
Deadline: time.Time{}, // provisionerd will set this upon success
Reason: database.BuildReasonInitiator,
})
if err != nil {
return xerrors.Errorf("insert workspace build: %w", err)
}
names := make([]string, 0, len(createWorkspace.RichParameterValues))
values := make([]string, 0, len(createWorkspace.RichParameterValues))
for _, param := range createWorkspace.RichParameterValues {
names = append(names, param.Name)
values = append(values, param.Value)
}
err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{
WorkspaceBuildID: workspaceBuildID,
Name: names,
Value: values,
})
if err != nil {
return xerrors.Errorf("insert workspace build parameters: %w", err)
}
return nil
return err
}, nil)
var bldErr wsbuilder.BuildError
if xerrors.As(err, &bldErr) {
httpapi.Write(ctx, rw, bldErr.Status, codersdk.Response{
Message: bldErr.Message,
Detail: bldErr.Error(),
})
return
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error creating workspace.",
@ -594,14 +462,14 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
api.Telemetry.Report(&telemetry.Snapshot{
Workspaces: []telemetry.Workspace{telemetry.ConvertWorkspace(workspace)},
WorkspaceBuilds: []telemetry.WorkspaceBuild{telemetry.ConvertWorkspaceBuild(workspaceBuild)},
WorkspaceBuilds: []telemetry.WorkspaceBuild{telemetry.ConvertWorkspaceBuild(*workspaceBuild)},
})
users := []database.User{user, initiator}
apiBuild, err := api.convertWorkspaceBuild(
workspaceBuild,
*workspaceBuild,
workspace,
provisionerJob,
*provisionerJob,
users,
[]database.WorkspaceResource{},
[]database.WorkspaceResourceMetadatum{},

View File

@ -746,9 +746,11 @@ func TestWorkspaceFilterManual(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version2.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
template2 := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
template2 := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template2.ID)
@ -819,9 +821,11 @@ func TestWorkspaceFilterManual(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version2.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
template2 := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
template2 := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template2.ID)
@ -2038,14 +2042,12 @@ func TestWorkspaceWithOptionalRichParameters(t *testing.T) {
require.Equal(t, secondParameterDescription, templateRichParameters[1].Description)
require.Equal(t, secondParameterRequired, templateRichParameters[1].Required)
expectedBuildParameters := []codersdk.WorkspaceBuildParameter{
// First parameter is optional, so coder will pick the default value.
{Name: secondParameterName, Value: secondParameterValue},
}
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = expectedBuildParameters
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
// First parameter is optional, so coder will pick the default value.
{Name: secondParameterName, Value: secondParameterValue},
}
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
@ -2054,5 +2056,10 @@ func TestWorkspaceWithOptionalRichParameters(t *testing.T) {
workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceBuild.ID)
require.NoError(t, err)
expectedBuildParameters := []codersdk.WorkspaceBuildParameter{
// Coderd inserts the default for the missing parameter
{Name: firstParameterName, Value: firstParameterDefaultValue},
{Name: secondParameterName, Value: secondParameterValue},
}
require.ElementsMatch(t, expectedBuildParameters, workspaceBuildParameters)
}

View File

@ -0,0 +1,764 @@
// 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"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/tabbed/pqtype"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/db2sdk"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/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
legacyParameterValues []codersdk.CreateParameterRequest
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
lastParameterValues *[]database.ParameterValue
lastBuildJob *database.ProvisionerJob
}
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) 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) LegacyParameterValues(p []codersdk.CreateParameterRequest) Builder {
// nolint: revive
b.legacyParameterValues = p
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,
) (
*database.WorkspaceBuild, *database.ProvisionerJob, error,
) {
b.ctx = ctx
// 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 err error
for retries := 0; retries < 5; retries++ {
var workspaceBuild *database.WorkspaceBuild
var provisionerJob *database.ProvisionerJob
err := store.InTx(func(store database.Store) error {
b.store = store
workspaceBuild, provisionerJob, err = b.buildTx(authFunc)
return err
}, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
var pqe *pq.Error
if xerrors.As(err, &pqe) {
if pqe.Code == "40001" {
// serialization error, retry
continue
}
}
if err != nil {
// Other (hard) error
return nil, nil, err
}
return workspaceBuild, provisionerJob, nil
}
return nil, nil, xerrors.Errorf("too many errors; last error: %w", err)
}
// 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,
}
}
legacyParameters, err := b.getLastParameterValues()
if err != nil {
return nil, nil, BuildError{
http.StatusInternalServerError,
"failed to fetch previous legacy parameters.",
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
}
// Write/Update any new params
now := database.Now()
for _, param := range b.legacyParameterValues {
for _, exists := range legacyParameters {
// If the param exists, delete the old param before inserting the new one
if exists.Name == param.Name {
err = b.store.DeleteParameterValueByID(b.ctx, exists.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return nil, nil, BuildError{
http.StatusInternalServerError,
fmt.Sprintf("Failed to delete old param %q", exists.Name),
err,
}
}
}
}
// If the value is empty, we don't want to save it on database so
// Terraform can use the default value
if param.SourceValue == "" {
continue
}
_, err = b.store.InsertParameterValue(b.ctx, database.InsertParameterValueParams{
ID: uuid.New(),
Name: param.Name,
CreatedAt: now,
UpdatedAt: now,
Scope: database.ParameterScopeWorkspace,
ScopeID: b.workspace.ID,
SourceScheme: database.ParameterSourceScheme(param.SourceScheme),
SourceValue: param.SourceValue,
DestinationScheme: database.ParameterDestinationScheme(param.DestinationScheme),
})
if err != nil {
return nil, nil, BuildError{http.StatusInternalServerError, "insert parameter value", err}
}
}
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 := provisionerdserver.MutateTags(b.workspace.OwnerID, templateVersionJob.Tags)
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}
}
workspaceBuild, err := b.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,
})
if err != nil {
return nil, nil, BuildError{http.StatusInternalServerError, "insert workspace build", err}
}
names, values, err := b.getParameters()
if err != nil {
// getParameters already wraps errors in BuildError
return nil, nil, err
}
err = b.store.InsertWorkspaceBuildParameters(b.ctx, database.InsertWorkspaceBuildParametersParams{
WorkspaceBuildID: workspaceBuildID,
Name: names,
Value: values,
})
if err != nil {
return nil, nil, BuildError{http.StatusInternalServerError, "insert workspace build parameters: %w", 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}
}
lastParameterValues, err := b.getLastParameterValues()
if err != nil {
return nil, nil, BuildError{http.StatusInternalServerError, "failed to fetch last parameter values", err}
}
resolver := codersdk.ParameterResolver{
Rich: db2sdk.WorkspaceBuildParameters(lastBuildParameters),
Legacy: db2sdk.Parameters(lastParameterValues),
}
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, err.Error(), 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
}
func (b *Builder) getLastParameterValues() ([]database.ParameterValue, error) {
if b.lastParameterValues != nil {
return *b.lastParameterValues, nil
}
pv, err := b.store.ParameterValues(b.ctx, database.ParameterValuesParams{
Scopes: []database.ParameterScope{database.ParameterScopeWorkspace},
ScopeIds: []uuid.UUID{b.workspace.ID},
})
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return nil, xerrors.Errorf("get workspace %w parameter values: %w", b.workspace.ID, err)
}
b.lastParameterValues = &pv
return pv, 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:
return BuildError{http.StatusBadRequest, fmt.Sprintf("Transition %q not supported.", b.trans), xerrors.New("")}
}
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("")}
}
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("")}
}
}
if b.logLevel != "" && !authFunc(rbac.ActionUpdate, template) {
return BuildError{
http.StatusBadRequest,
"Workspace builds with a custom log level are restricted to template authors only.",
xerrors.New(""),
}
}
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 := db2sdk.ProvisionerJobStatus(*templateVersionJob)
switch templateVersionJobStatus {
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
return BuildError{
http.StatusNotAcceptable,
fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus),
xerrors.New(""),
}
case codersdk.ProvisionerJobFailed:
return BuildError{
http.StatusBadRequest,
fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String),
xerrors.New(""),
}
case codersdk.ProvisionerJobCanceled:
return BuildError{
http.StatusBadRequest,
"The provided template version was canceled during import. You cannot build workspaces with it!",
xerrors.New(""),
}
}
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 db2sdk.ProvisionerJobStatus(*job).Active() {
return BuildError{
http.StatusConflict,
"A workspace build is already active.",
xerrors.New(""),
}
}
return nil
}

View File

@ -0,0 +1,839 @@
package wsbuilder_test
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbmock"
"github.com/coder/coder/coderd/database/dbtype"
"github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/wsbuilder"
"github.com/coder/coder/codersdk"
)
var (
// use fixed IDs so logs are easier to read
templateID = uuid.MustParse("12341234-0000-0000-0001-000000000000")
activeVersionID = uuid.MustParse("12341234-0000-0000-0002-000000000000")
inactiveVersionID = uuid.MustParse("12341234-0000-0000-0003-000000000000")
activeJobID = uuid.MustParse("12341234-0000-0000-0004-000000000000")
inactiveJobID = uuid.MustParse("12341234-0000-0000-0005-000000000000")
orgID = uuid.MustParse("12341234-0000-0000-0006-000000000000")
workspaceID = uuid.MustParse("12341234-0000-0000-0007-000000000000")
userID = uuid.MustParse("12341234-0000-0000-0008-000000000000")
activeFileID = uuid.MustParse("12341234-0000-0000-0009-000000000000")
inactiveFileID = uuid.MustParse("12341234-0000-0000-000a-000000000000")
lastBuildID = uuid.MustParse("12341234-0000-0000-000b-000000000000")
lastBuildJobID = uuid.MustParse("12341234-0000-0000-000c-000000000000")
otherUserID = uuid.MustParse("12341234-0000-0000-000d-000000000000")
notReplacedParamID = uuid.MustParse("12341234-0000-0000-000e-000000000000")
replacedParamID = uuid.MustParse("12341234-0000-0000-000f-000000000000")
)
func TestBuilder_NoOptions(t *testing.T) {
t.Parallel()
req := require.New(t)
asrt := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var buildID uuid.UUID
mDB := expectDB(t,
// Inputs
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withLegacyParameters(nil), withRichParameters(nil),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
asrt.Equal(userID, job.InitiatorID)
asrt.Equal(inactiveFileID, job.FileID)
input := provisionerdserver.WorkspaceProvisionJob{}
err := json.Unmarshal(job.Input, &input)
req.NoError(err)
// store build ID for later
buildID = input.WorkspaceBuildID
}),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {
asrt.Equal(inactiveVersionID, bld.TemplateVersionID)
asrt.Equal(workspaceID, bld.WorkspaceID)
asrt.Equal(int32(2), bld.BuildNumber)
asrt.Equal("last build state", string(bld.ProvisionerState))
asrt.Equal(userID, bld.InitiatorID)
asrt.Equal(database.WorkspaceTransitionStart, bld.Transition)
asrt.Equal(database.BuildReasonInitiator, bld.Reason)
asrt.Equal(buildID, bld.ID)
}),
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
asrt.Equal(buildID, params.WorkspaceBuildID)
asrt.Empty(params.Name)
asrt.Empty(params.Value)
}),
)
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart)
_, _, err := uut.Build(ctx, mDB, nil)
req.NoError(err)
}
func TestBuilder_Initiator(t *testing.T) {
t.Parallel()
req := require.New(t)
asrt := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mDB := expectDB(t,
// Inputs
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withLegacyParameters(nil), withRichParameters(nil),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
asrt.Equal(otherUserID, job.InitiatorID)
}),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {
asrt.Equal(otherUserID, bld.InitiatorID)
}),
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
}),
)
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).Initiator(otherUserID)
_, _, err := uut.Build(ctx, mDB, nil)
req.NoError(err)
}
func TestBuilder_Reason(t *testing.T) {
t.Parallel()
req := require.New(t)
asrt := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mDB := expectDB(t,
// Inputs
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withLegacyParameters(nil), withRichParameters(nil),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
}),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {
asrt.Equal(database.BuildReasonAutostart, bld.Reason)
}),
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
}),
)
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).Reason(database.BuildReasonAutostart)
_, _, err := uut.Build(ctx, mDB, nil)
req.NoError(err)
}
func TestBuilder_ActiveVersion(t *testing.T) {
t.Parallel()
req := require.New(t)
asrt := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mDB := expectDB(t,
// Inputs
withTemplate,
withActiveVersion(nil),
withLastBuildNotFound,
withLegacyParameters(nil),
// previous rich parameters are not queried because there is no previous build.
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
asrt.Equal(activeFileID, job.FileID)
}),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {
asrt.Equal(activeVersionID, bld.TemplateVersionID)
// no previous build...
asrt.Equal(int32(1), bld.BuildNumber)
asrt.Len(bld.ProvisionerState, 0)
}),
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
}),
)
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).ActiveVersion()
_, _, err := uut.Build(ctx, mDB, nil)
req.NoError(err)
}
func TestBuilder_LegacyParams(t *testing.T) {
t.Parallel()
req := require.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
oldParams := []database.ParameterValue{
{Name: "not-replaced", SourceValue: "nr", ID: notReplacedParamID},
{Name: "replaced", SourceValue: "r", ID: replacedParamID},
}
newParams := []codersdk.CreateParameterRequest{
{Name: "replaced", SourceValue: "s"},
{Name: "new", SourceValue: "n"},
}
mDB := expectDB(t,
// Inputs
withTemplate,
withActiveVersion(nil),
withLastBuildFound,
withLegacyParameters(oldParams),
withRichParameters(nil),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
}),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {
}),
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
}),
expectReplacedParam(replacedParamID, "replaced", "s"),
expectInsertedParam("new", "n"),
)
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).ActiveVersion().LegacyParameterValues(newParams)
_, _, err := uut.Build(ctx, mDB, nil)
req.NoError(err)
}
func TestWorkspaceBuildWithRichParameters(t *testing.T) {
t.Parallel()
const (
firstParameterName = "first_parameter"
firstParameterDescription = "This is first parameter"
firstParameterValue = "1"
secondParameterName = "second_parameter"
secondParameterDescription = "This is second parameter"
secondParameterValue = "2"
immutableParameterName = "immutable_parameter"
immutableParameterDescription = "This is immutable parameter"
immutableParameterValue = "3"
)
initialBuildParameters := []database.WorkspaceBuildParameter{
{Name: firstParameterName, Value: firstParameterValue},
{Name: secondParameterName, Value: secondParameterValue},
{Name: immutableParameterName, Value: immutableParameterValue},
}
richParameters := []database.TemplateVersionParameter{
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true, Options: json.RawMessage("[]")},
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true, Options: json.RawMessage("[]")},
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false, Options: json.RawMessage("[]")},
}
t.Run("UpdateParameterValues", func(t *testing.T) {
t.Parallel()
req := require.New(t)
asrt := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const updatedParameterValue = "3"
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: firstParameterName, Value: firstParameterValue},
{Name: secondParameterName, Value: updatedParameterValue},
}
expectedParams := map[string]string{
firstParameterName: firstParameterValue,
secondParameterName: updatedParameterValue,
immutableParameterName: immutableParameterValue,
}
mDB := expectDB(t,
// Inputs
withTemplate,
withInactiveVersion(richParameters),
withLastBuildFound,
withLegacyParameters(nil),
withRichParameters(initialBuildParameters),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {}),
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
asrt.Len(params.Name, len(expectedParams))
for i := range params.Name {
value, ok := expectedParams[params.Name[i]]
asrt.True(ok, "unexpected name %s", params.Name[i])
asrt.Equal(value, params.Value[i])
}
}),
)
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters)
_, _, err := uut.Build(ctx, mDB, nil)
req.NoError(err)
})
t.Run("UsePreviousParameterValues", func(t *testing.T) {
t.Parallel()
req := require.New(t)
asrt := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
nextBuildParameters := []codersdk.WorkspaceBuildParameter{}
expectedParams := map[string]string{}
for _, p := range initialBuildParameters {
expectedParams[p.Name] = p.Value
}
mDB := expectDB(t,
// Inputs
withTemplate,
withInactiveVersion(richParameters),
withLastBuildFound,
withLegacyParameters(nil),
withRichParameters(initialBuildParameters),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {}),
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
asrt.Len(params.Name, len(expectedParams))
for i := range params.Name {
value, ok := expectedParams[params.Name[i]]
asrt.True(ok, "unexpected name %s", params.Name[i])
asrt.Equal(value, params.Value[i])
}
}),
)
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters)
_, _, err := uut.Build(ctx, mDB, nil)
req.NoError(err)
})
t.Run("DoNotModifyImmutables", func(t *testing.T) {
t.Parallel()
req := require.New(t)
asrt := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: immutableParameterName, Value: "BAD"},
}
mDB := expectDB(t,
// Inputs
withTemplate,
withInactiveVersion(richParameters),
withLastBuildFound,
withLegacyParameters(nil),
withRichParameters(initialBuildParameters),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {}),
// no build parameters, since we hit an error validating.
// expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {}),
)
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters)
_, _, err := uut.Build(ctx, mDB, nil)
bldErr := wsbuilder.BuildError{}
req.ErrorAs(err, &bldErr)
asrt.Equal(http.StatusBadRequest, bldErr.Status)
})
t.Run("NewImmutableRequiredParameterAdded", func(t *testing.T) {
t.Parallel()
req := require.New(t)
asrt := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// new template revision
const newImmutableParameterName = "new_immutable_parameter"
const newImmutableParameterDescription = "This is also an immutable parameter"
version2params := []database.TemplateVersionParameter{
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true, Options: json.RawMessage("[]")},
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true, Options: json.RawMessage("[]")},
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false, Options: json.RawMessage("[]")},
{Name: newImmutableParameterName, Description: newImmutableParameterDescription, Mutable: false, Required: true, Options: json.RawMessage("[]")},
}
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: newImmutableParameterName, Value: "good"},
}
expectedParams := map[string]string{
firstParameterName: firstParameterValue,
secondParameterName: secondParameterValue,
immutableParameterName: immutableParameterValue,
newImmutableParameterName: "good",
}
mDB := expectDB(t,
// Inputs
withTemplate,
withActiveVersion(version2params),
withLastBuildFound,
withLegacyParameters(nil),
withRichParameters(initialBuildParameters),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {}),
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
asrt.Len(params.Name, len(expectedParams))
for i := range params.Name {
value, ok := expectedParams[params.Name[i]]
asrt.True(ok, "unexpected name %s", params.Name[i])
asrt.Equal(value, params.Value[i])
}
}),
)
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).
RichParameterValues(nextBuildParameters).
VersionID(activeVersionID)
_, _, err := uut.Build(ctx, mDB, nil)
req.NoError(err)
})
t.Run("NewImmutableOptionalParameterAdded", func(t *testing.T) {
t.Parallel()
req := require.New(t)
asrt := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// new template revision
const newImmutableParameterName = "new_immutable_parameter"
const newImmutableParameterDescription = "This is also an immutable parameter"
version2params := []database.TemplateVersionParameter{
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true, Options: json.RawMessage("[]")},
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true, Options: json.RawMessage("[]")},
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false, Options: json.RawMessage("[]")},
{Name: newImmutableParameterName, Description: newImmutableParameterDescription, Mutable: false, DefaultValue: "12345", Options: json.RawMessage("[]")},
}
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: newImmutableParameterName, Value: "good"},
}
expectedParams := map[string]string{
firstParameterName: firstParameterValue,
secondParameterName: secondParameterValue,
immutableParameterName: immutableParameterValue,
newImmutableParameterName: "good",
}
mDB := expectDB(t,
// Inputs
withTemplate,
withActiveVersion(version2params),
withLastBuildFound,
withLegacyParameters(nil),
withRichParameters(initialBuildParameters),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {}),
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
asrt.Len(params.Name, len(expectedParams))
for i := range params.Name {
value, ok := expectedParams[params.Name[i]]
asrt.True(ok, "unexpected name %s", params.Name[i])
asrt.Equal(value, params.Value[i])
}
}),
)
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).
RichParameterValues(nextBuildParameters).
VersionID(activeVersionID)
_, _, err := uut.Build(ctx, mDB, nil)
req.NoError(err)
})
t.Run("NewImmutableOptionalParameterUsesDefault", func(t *testing.T) {
t.Parallel()
req := require.New(t)
asrt := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// new template revision
const newImmutableParameterName = "new_immutable_parameter"
const newImmutableParameterDescription = "This is also an immutable parameter"
version2params := []database.TemplateVersionParameter{
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true, Options: json.RawMessage("[]")},
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true, Options: json.RawMessage("[]")},
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false, Options: json.RawMessage("[]")},
{Name: newImmutableParameterName, Description: newImmutableParameterDescription, Mutable: false, DefaultValue: "12345", Options: json.RawMessage("[]")},
}
nextBuildParameters := []codersdk.WorkspaceBuildParameter{}
expectedParams := map[string]string{
firstParameterName: firstParameterValue,
secondParameterName: secondParameterValue,
immutableParameterName: immutableParameterValue,
newImmutableParameterName: "12345",
}
mDB := expectDB(t,
// Inputs
withTemplate,
withActiveVersion(version2params),
withLastBuildFound,
withLegacyParameters(nil),
withRichParameters(initialBuildParameters),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {}),
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
asrt.Len(params.Name, len(expectedParams))
for i := range params.Name {
value, ok := expectedParams[params.Name[i]]
asrt.True(ok, "unexpected name %s", params.Name[i])
asrt.Equal(value, params.Value[i])
}
}),
)
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).
RichParameterValues(nextBuildParameters).
VersionID(activeVersionID)
_, _, err := uut.Build(ctx, mDB, nil)
req.NoError(err)
})
}
type txExpect func(mTx *dbmock.MockStore)
func expectDB(t *testing.T, opts ...txExpect) *dbmock.MockStore {
t.Helper()
ctrl := gomock.NewController(t)
mDB := dbmock.NewMockStore(ctrl)
mTx := dbmock.NewMockStore(ctrl)
// we expect to be run in a transaction; we use mTx to record the
// "in transaction" calls.
mDB.EXPECT().InTx(
gomock.Any(), gomock.Eq(&sql.TxOptions{Isolation: sql.LevelRepeatableRead}),
).
DoAndReturn(func(f func(database.Store) error, _ *sql.TxOptions) error {
err := f(mTx)
return err
})
// txExpect args set up the expectations for what happens in the transaction.
for _, o := range opts {
o(mTx)
}
return mDB
}
func withTemplate(mTx *dbmock.MockStore) {
mTx.EXPECT().GetTemplateByID(gomock.Any(), templateID).
Times(1).
Return(database.Template{
ID: templateID,
OrganizationID: orgID,
Provisioner: database.ProvisionerTypeTerraform,
ActiveVersionID: activeVersionID,
}, nil)
}
func withActiveVersion(params []database.TemplateVersionParameter) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
mTx.EXPECT().GetTemplateVersionByID(gomock.Any(), activeVersionID).
Times(1).
Return(database.TemplateVersion{
ID: activeVersionID,
TemplateID: uuid.NullUUID{UUID: templateID, Valid: true},
OrganizationID: orgID,
Name: "active",
JobID: activeJobID,
}, nil)
mTx.EXPECT().GetProvisionerJobByID(gomock.Any(), activeJobID).
Times(1).Return(database.ProvisionerJob{
ID: activeJobID,
OrganizationID: orgID,
InitiatorID: userID,
Provisioner: database.ProvisionerTypeTerraform,
StorageMethod: database.ProvisionerStorageMethodFile,
Type: database.ProvisionerJobTypeTemplateVersionImport,
Input: nil,
Tags: dbtype.StringMap{
"version": "active",
provisionerdserver.TagScope: provisionerdserver.ScopeUser,
},
FileID: activeFileID,
StartedAt: sql.NullTime{Time: database.Now(), Valid: true},
UpdatedAt: time.Now(),
CompletedAt: sql.NullTime{Time: database.Now(), Valid: true},
}, nil)
paramsCall := mTx.EXPECT().GetTemplateVersionParameters(gomock.Any(), activeVersionID).
Times(1)
if len(params) > 0 {
paramsCall.Return(params, nil)
} else {
paramsCall.Return(nil, sql.ErrNoRows)
}
}
}
func withInactiveVersion(params []database.TemplateVersionParameter) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
mTx.EXPECT().GetTemplateVersionByID(gomock.Any(), inactiveVersionID).
Times(1).
Return(database.TemplateVersion{
ID: inactiveVersionID,
TemplateID: uuid.NullUUID{UUID: templateID, Valid: true},
OrganizationID: orgID,
Name: "inactive",
JobID: inactiveJobID,
}, nil)
mTx.EXPECT().GetProvisionerJobByID(gomock.Any(), inactiveJobID).
Times(1).Return(database.ProvisionerJob{
ID: inactiveJobID,
OrganizationID: orgID,
InitiatorID: userID,
Provisioner: database.ProvisionerTypeTerraform,
StorageMethod: database.ProvisionerStorageMethodFile,
Type: database.ProvisionerJobTypeTemplateVersionImport,
Input: nil,
Tags: dbtype.StringMap{
"version": "inactive",
provisionerdserver.TagScope: provisionerdserver.ScopeUser,
},
FileID: inactiveFileID,
StartedAt: sql.NullTime{Time: database.Now(), Valid: true},
UpdatedAt: time.Now(),
CompletedAt: sql.NullTime{Time: database.Now(), Valid: true},
}, nil)
paramsCall := mTx.EXPECT().GetTemplateVersionParameters(gomock.Any(), inactiveVersionID).
Times(1)
if len(params) > 0 {
paramsCall.Return(params, nil)
} else {
paramsCall.Return(nil, sql.ErrNoRows)
}
}
}
func withLastBuildFound(mTx *dbmock.MockStore) {
mTx.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), workspaceID).
Times(1).
Return(database.WorkspaceBuild{
ID: lastBuildID,
WorkspaceID: workspaceID,
TemplateVersionID: inactiveVersionID,
BuildNumber: 1,
Transition: database.WorkspaceTransitionStart,
InitiatorID: userID,
JobID: lastBuildJobID,
ProvisionerState: []byte("last build state"),
Reason: database.BuildReasonInitiator,
}, nil)
mTx.EXPECT().GetProvisionerJobByID(gomock.Any(), lastBuildJobID).
Times(1).
Return(database.ProvisionerJob{
ID: lastBuildJobID,
OrganizationID: orgID,
InitiatorID: userID,
Provisioner: database.ProvisionerTypeTerraform,
StorageMethod: database.ProvisionerStorageMethodFile,
FileID: inactiveFileID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
StartedAt: sql.NullTime{Time: database.Now(), Valid: true},
UpdatedAt: time.Now(),
CompletedAt: sql.NullTime{Time: database.Now(), Valid: true},
}, nil)
}
func withLastBuildNotFound(mTx *dbmock.MockStore) {
mTx.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), workspaceID).
Times(1).
Return(database.WorkspaceBuild{}, sql.ErrNoRows)
}
func withLegacyParameters(params []database.ParameterValue) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
c := mTx.EXPECT().ParameterValues(
gomock.Any(),
database.ParameterValuesParams{
Scopes: []database.ParameterScope{database.ParameterScopeWorkspace},
ScopeIds: []uuid.UUID{workspaceID},
}).
Times(1)
if len(params) > 0 {
c.Return(params, nil)
} else {
c.Return(nil, sql.ErrNoRows)
}
}
}
func withRichParameters(params []database.WorkspaceBuildParameter) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
c := mTx.EXPECT().GetWorkspaceBuildParameters(gomock.Any(), lastBuildID).
Times(1)
if len(params) > 0 {
c.Return(params, nil)
} else {
c.Return(nil, sql.ErrNoRows)
}
}
}
// Since there is expected to be only one each of job, build, and build-parameters inserted, instead
// of building matchers, we match any call and then assert its parameters. This will feel
// more familiar to the way we write other tests.
// expectProvisionerJob captures a call to InsertProvisionerJob and runs the provided assertions
// against it.
func expectProvisionerJob(
assertions func(job database.InsertProvisionerJobParams),
) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
mTx.EXPECT().InsertProvisionerJob(gomock.Any(), gomock.Any()).
Times(1).
DoAndReturn(
func(ctx context.Context, params database.InsertProvisionerJobParams) (database.ProvisionerJob, error) {
assertions(params)
// there is no point copying anything other than the ID, since this object is just
// returned to our test code, and we've already asserted what we care about.
return database.ProvisionerJob{ID: params.ID}, nil
},
)
}
}
// expectBuild captures a call to InsertWorkspaceBuild and runs the provided assertions
// against it.
func expectBuild(
assertions func(job database.InsertWorkspaceBuildParams),
) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
mTx.EXPECT().InsertWorkspaceBuild(gomock.Any(), gomock.Any()).
Times(1).
DoAndReturn(
func(ctx context.Context, params database.InsertWorkspaceBuildParams) (database.WorkspaceBuild, error) {
assertions(params)
// there is no point copying anything other than the ID, since this object is just
// returned to our test code, and we've already asserted what we care about.
return database.WorkspaceBuild{ID: params.ID}, nil
},
)
}
}
// expectBuildParameters captures a call to InsertWorkspaceBuildParameters and runs the provided assertions
// against it.
func expectBuildParameters(
assertions func(database.InsertWorkspaceBuildParametersParams),
) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
mTx.EXPECT().InsertWorkspaceBuildParameters(gomock.Any(), gomock.Any()).
Times(1).
DoAndReturn(
func(ctx context.Context, params database.InsertWorkspaceBuildParametersParams) error {
assertions(params)
return nil
},
)
}
}
type insertParameterMatcher struct {
name string
value string
}
func (m insertParameterMatcher) Matches(x interface{}) bool {
p, ok := x.(database.InsertParameterValueParams)
if !ok {
return false
}
if p.Name != m.name {
return false
}
return p.SourceValue == m.value
}
func (m insertParameterMatcher) String() string {
return fmt.Sprintf("ParameterValue %s=%s", m.name, m.value)
}
func expectReplacedParam(oldID uuid.UUID, name, newValue string) func(store *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
del := mTx.EXPECT().DeleteParameterValueByID(gomock.Any(), oldID).
Times(1).
Return(nil)
mTx.EXPECT().InsertParameterValue(gomock.Any(), insertParameterMatcher{name, newValue}).
Times(1).
After(del).
Return(database.ParameterValue{}, nil)
}
}
func expectInsertedParam(name, newValue string) func(store *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
mTx.EXPECT().InsertParameterValue(gomock.Any(), insertParameterMatcher{name, newValue}).
Times(1).
Return(database.ParameterValue{}, nil)
}
}

View File

@ -43,7 +43,6 @@ const (
type ComputedParameter struct {
Parameter
SourceValue string `json:"source_value"`
SchemaID uuid.UUID `json:"schema_id" format:"uuid"`
DefaultSourceValue bool `json:"default_source_value"`
}
@ -60,6 +59,7 @@ type Parameter struct {
DestinationScheme ParameterDestinationScheme `json:"destination_scheme" table:"destination scheme" validate:"ne=none" enums:"none,environment_variable,provisioner_variable"`
CreatedAt time.Time `json:"created_at" table:"created at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" table:"updated at" format:"date-time"`
SourceValue string `json:"source_value"`
}
type ParameterSchema struct {

View File

@ -116,3 +116,64 @@ func validationEnabled(param TemplateVersionParameter) bool {
param.Type == "bool" || // boolean type doesn't have any custom validation rules, but the value must be checked (true/false).
param.Type == "list(string)" // list(string) type doesn't have special validation, but we need to check if this is a correct list.
}
// ParameterResolver should be populated with legacy workload and rich parameter values from the previous build. It then
// supports queries against a current TemplateVersionParameter to determine whether a new value is required, or a value
// correctly validates.
// @typescript-ignore ParameterResolver
type ParameterResolver struct {
Legacy []Parameter
Rich []WorkspaceBuildParameter
}
// ValidateResolve checks the provided value, v, against the parameter, p, and the previous build. If v is nil, it also
// resolves the correct value. It returns the value of the parameter, if valid, and an error if invalid.
func (r *ParameterResolver) ValidateResolve(p TemplateVersionParameter, v *WorkspaceBuildParameter) (value string, err error) {
prevV := r.findLastValue(p)
if !p.Mutable && v != nil && prevV != nil {
return "", xerrors.Errorf("Parameter %q is not mutable, so it can't be updated after creating a workspace.", p.Name)
}
if p.Required && v == nil && prevV == nil {
return "", xerrors.Errorf("Parameter %q is required but not provided", p.Name)
}
// First, the provided value
resolvedValue := v
// Second, previous value
if resolvedValue == nil {
resolvedValue = prevV
}
// Last, default value
if resolvedValue == nil {
resolvedValue = &WorkspaceBuildParameter{
Name: p.Name,
Value: p.DefaultValue,
}
}
err = ValidateWorkspaceBuildParameter(p, resolvedValue, prevV)
if err != nil {
return "", err
}
return resolvedValue.Value, nil
}
// findLastValue finds the value from the previous build and returns it, or nil if the parameter had no value in the
// last build.
func (r *ParameterResolver) findLastValue(p TemplateVersionParameter) *WorkspaceBuildParameter {
for _, rp := range r.Rich {
if rp.Name == p.Name {
return &rp
}
}
// For migration purposes, we also support using a legacy variable
if p.LegacyVariableName != "" {
for _, lp := range r.Legacy {
if lp.Name == p.LegacyVariableName {
return &WorkspaceBuildParameter{
Name: p.Name,
Value: lp.SourceValue,
}
}
}
}
return nil
}

View File

@ -0,0 +1,324 @@
package codersdk_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/codersdk"
)
func TestParameterResolver_ValidateResolve_New(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
}
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: "n",
Value: "1",
})
require.NoError(t, err)
require.Equal(t, "1", v)
}
func TestParameterResolver_ValidateResolve_Default(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
DefaultValue: "5",
}
v, err := uut.ValidateResolve(p, nil)
require.NoError(t, err)
require.Equal(t, "5", v)
}
func TestParameterResolver_ValidateResolve_MissingRequired(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
}
v, err := uut.ValidateResolve(p, nil)
require.Error(t, err)
require.Equal(t, "", v)
}
func TestParameterResolver_ValidateResolve_PrevRequired(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "5"}},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
}
v, err := uut.ValidateResolve(p, nil)
require.NoError(t, err)
require.Equal(t, "5", v)
}
func TestParameterResolver_ValidateResolve_PrevInvalid(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "11"}},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
ValidationMax: 10,
ValidationMin: 1,
}
v, err := uut.ValidateResolve(p, nil)
require.Error(t, err)
require.Equal(t, "", v)
}
func TestParameterResolver_ValidateResolve_DefaultInvalid(t *testing.T) {
// this one arises from an error on the template itself, where the default
// value doesn't pass validation. But, it's good to catch early and error out
// rather than send invalid data to the provisioner
t.Parallel()
uut := codersdk.ParameterResolver{}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
ValidationMax: 10,
ValidationMin: 1,
DefaultValue: "11",
}
v, err := uut.ValidateResolve(p, nil)
require.Error(t, err)
require.Equal(t, "", v)
}
func TestParameterResolver_ValidateResolve_NewOverridesOld(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "5"}},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
Mutable: true,
}
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: "n",
Value: "6",
})
require.NoError(t, err)
require.Equal(t, "6", v)
}
func TestParameterResolver_ValidateResolve_Immutable(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "5"}},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
Mutable: false,
}
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: "n",
Value: "6",
})
require.Error(t, err)
require.Equal(t, "", v)
}
func TestParameterResolver_ValidateResolve_Legacy(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Legacy: []codersdk.Parameter{
{Name: "l", SourceValue: "5"},
{Name: "n", SourceValue: "6"},
},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
LegacyVariableName: "l",
}
v, err := uut.ValidateResolve(p, nil)
require.NoError(t, err)
require.Equal(t, "5", v)
}
func TestParameterResolver_ValidateResolve_PreferRichOverLegacy(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "7"}},
Legacy: []codersdk.Parameter{
{Name: "l", SourceValue: "5"},
{Name: "n", SourceValue: "6"},
},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
LegacyVariableName: "l",
}
v, err := uut.ValidateResolve(p, nil)
require.NoError(t, err)
require.Equal(t, "7", v)
}
func TestRichParameterValidation(t *testing.T) {
t.Parallel()
const (
stringParameterName = "string_parameter"
stringParameterValue = "abc"
numberParameterName = "number_parameter"
numberParameterValue = "7"
boolParameterName = "bool_parameter"
boolParameterValue = "true"
listOfStringsParameterName = "list_of_strings_parameter"
listOfStringsParameterValue = `["a","b","c"]`
)
initialBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: stringParameterName, Value: stringParameterValue},
{Name: numberParameterName, Value: numberParameterValue},
{Name: boolParameterName, Value: boolParameterValue},
{Name: listOfStringsParameterName, Value: listOfStringsParameterValue},
}
t.Run("NoValidation", func(t *testing.T) {
t.Parallel()
p := codersdk.TemplateVersionParameter{
Name: numberParameterName, Type: "number", Mutable: true,
}
uut := codersdk.ParameterResolver{
Rich: initialBuildParameters,
}
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: numberParameterName, Value: "42",
})
require.NoError(t, err)
require.Equal(t, v, "42")
})
t.Run("Validation", func(t *testing.T) {
t.Parallel()
numberRichParameters := []codersdk.TemplateVersionParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
monotonicIncreasingNumberRichParameters := []codersdk.TemplateVersionParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10, ValidationMonotonic: "increasing"},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
monotonicDecreasingNumberRichParameters := []codersdk.TemplateVersionParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10, ValidationMonotonic: "decreasing"},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
stringRichParameters := []codersdk.TemplateVersionParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
boolRichParameters := []codersdk.TemplateVersionParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
regexRichParameters := []codersdk.TemplateVersionParameter{
{Name: stringParameterName, Type: "string", Mutable: true, ValidationRegex: "^[a-z]+$", ValidationError: "this is error"},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
listOfStringsRichParameters := []codersdk.TemplateVersionParameter{
{Name: listOfStringsParameterName, Type: "list(string)", Mutable: true},
}
tests := []struct {
parameterName string
value string
valid bool
richParameters []codersdk.TemplateVersionParameter
}{
{numberParameterName, "2", false, numberRichParameters},
{numberParameterName, "3", true, numberRichParameters},
{numberParameterName, "10", true, numberRichParameters},
{numberParameterName, "11", false, numberRichParameters},
{numberParameterName, "6", false, monotonicIncreasingNumberRichParameters},
{numberParameterName, "7", true, monotonicIncreasingNumberRichParameters},
{numberParameterName, "8", true, monotonicIncreasingNumberRichParameters},
{numberParameterName, "6", true, monotonicDecreasingNumberRichParameters},
{numberParameterName, "7", true, monotonicDecreasingNumberRichParameters},
{numberParameterName, "8", false, monotonicDecreasingNumberRichParameters},
{stringParameterName, "", true, stringRichParameters},
{stringParameterName, "foobar", true, stringRichParameters},
{stringParameterName, "abcd", true, regexRichParameters},
{stringParameterName, "abcd1", false, regexRichParameters},
{boolParameterName, "true", true, boolRichParameters},
{boolParameterName, "false", true, boolRichParameters},
{boolParameterName, "cat", false, boolRichParameters},
{listOfStringsParameterName, `[]`, true, listOfStringsRichParameters},
{listOfStringsParameterName, `["aa"]`, true, listOfStringsRichParameters},
{listOfStringsParameterName, `["aa]`, false, listOfStringsRichParameters},
{listOfStringsParameterName, ``, false, listOfStringsRichParameters},
}
for _, tc := range tests {
tc := tc
t.Run(tc.parameterName+"-"+tc.value, func(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: initialBuildParameters,
}
for _, p := range tc.richParameters {
if p.Name != tc.parameterName {
continue
}
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: tc.parameterName,
Value: tc.value,
})
if tc.valid {
require.NoError(t, err)
require.Equal(t, tc.value, v)
} else {
require.Error(t, err)
}
}
})
}
})
}

View File

@ -42,6 +42,7 @@ curl -X GET http://coder-server:8080/api/v2/parameters/{scope}/{id} \
"scope": "template",
"scope_id": "5d3fe357-12dd-4f62-b004-6d1fb3b8454f",
"source_scheme": "none",
"source_value": "string",
"updated_at": "2019-08-24T14:15:22Z"
}
]
@ -67,6 +68,7 @@ Status Code **200**
| `» scope` | [codersdk.ParameterScope](schemas.md#codersdkparameterscope) | false | | |
| `» scope_id` | string(uuid) | false | | |
| `» source_scheme` | [codersdk.ParameterSourceScheme](schemas.md#codersdkparametersourcescheme) | false | | |
| `» source_value` | string | false | | |
| `» updated_at` | string(date-time) | false | | |
#### Enumerated Values
@ -139,6 +141,7 @@ curl -X POST http://coder-server:8080/api/v2/parameters/{scope}/{id} \
"scope": "template",
"scope_id": "5d3fe357-12dd-4f62-b004-6d1fb3b8454f",
"source_scheme": "none",
"source_value": "string",
"updated_at": "2019-08-24T14:15:22Z"
}
```

View File

@ -3083,6 +3083,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
"scope": "template",
"scope_id": "5d3fe357-12dd-4f62-b004-6d1fb3b8454f",
"source_scheme": "none",
"source_value": "string",
"updated_at": "2019-08-24T14:15:22Z"
}
```
@ -3100,6 +3101,7 @@ Parameter represents a set value for the scope.
| `scope` | [codersdk.ParameterScope](#codersdkparameterscope) | false | | |
| `scope_id` | string | false | | |
| `source_scheme` | [codersdk.ParameterSourceScheme](#codersdkparametersourcescheme) | false | | |
| `source_value` | string | false | | |
| `updated_at` | string | false | | |
#### Enumerated Values

View File

@ -72,7 +72,8 @@ RUN mkdir --parents "$GOPATH" && \
# yq v4 is used to process yaml files in coder v2. Conflicts with
# yq v3 used in v1.
go install github.com/mikefarah/yq/v4@v4.30.6 && \
mv /tmp/bin/yq /tmp/bin/yq4
mv /tmp/bin/yq /tmp/bin/yq4 && \
go install github.com/golang/mock/mockgen@v1.6.0
FROM alpine:3.16 as proto
WORKDIR /tmp

1
go.mod
View File

@ -174,6 +174,7 @@ require (
nhooyr.io/websocket v1.8.7
storj.io/drpc v0.0.33-0.20230420154621-9716137f6037
tailscale.com v1.32.2
github.com/golang/mock v1.6.0
)
require github.com/go-chi/cors v1.2.1

1
go.sum
View File

@ -757,6 +757,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

View File

@ -141,7 +141,6 @@ export interface BuildInfoResponse {
// From codersdk/parameters.go
export interface ComputedParameter extends Parameter {
readonly source_value: string
readonly schema_id: string
readonly default_source_value: boolean
}
@ -590,6 +589,7 @@ export interface Parameter {
readonly destination_scheme: ParameterDestinationScheme
readonly created_at: string
readonly updated_at: string
readonly source_value: string
}
// From codersdk/parameters.go