mirror of https://github.com/coder/coder.git
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:
parent
622456faf8
commit
cd416c86dd
|
@ -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: |
|
||||
|
|
6
Makefile
6
Makefile
|
@ -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=. \
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -8063,6 +8063,9 @@ const docTemplate = `{
|
|||
}
|
||||
]
|
||||
},
|
||||
"source_value": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
|
|
|
@ -7206,6 +7206,9 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"source_value": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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{},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
1
go.mod
|
@ -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
1
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue