mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
feat: Add "coder projects create" command (#246)
* Refactor parameter parsing to return nil values if none computed * Refactor parameter to allow for hiding redisplay * Refactor parameters to enable schema matching * Refactor provisionerd to dynamically update parameter schemas * Refactor job update for provisionerd * Handle multiple states correctly when provisioning a project * Add project import job resource table * Basic creation flow works! * Create project fully works!!! * Only show job status if completed * Add create workspace support * Replace Netflix/go-expect with ActiveState * Fix linting errors * Use forked chzyer/readline * Add create workspace CLI * Add CLI test * Move jobs to their own APIs * Remove go-expect * Fix requested changes * Skip workspacecreate test on windows
This commit is contained in:
215
coderd/parameter/compute.go
Normal file
215
coderd/parameter/compute.go
Normal file
@ -0,0 +1,215 @@
|
||||
package parameter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
const (
|
||||
CoderUsername = "coder_username"
|
||||
CoderWorkspaceTransition = "coder_workspace_transition"
|
||||
)
|
||||
|
||||
// ComputeScope targets identifiers to pull parameters from.
|
||||
type ComputeScope struct {
|
||||
ProjectImportJobID uuid.UUID
|
||||
OrganizationID string
|
||||
UserID string
|
||||
ProjectID uuid.NullUUID
|
||||
WorkspaceID uuid.NullUUID
|
||||
}
|
||||
|
||||
type ComputeOptions struct {
|
||||
// HideRedisplayValues removes the value from parameters that
|
||||
// come from schemas with RedisplayValue set to false.
|
||||
HideRedisplayValues bool
|
||||
}
|
||||
|
||||
// ComputedValue represents a computed parameter value.
|
||||
type ComputedValue struct {
|
||||
database.ParameterValue
|
||||
SchemaID uuid.UUID `json:"schema_id"`
|
||||
DefaultSourceValue bool `json:"default_source_value"`
|
||||
}
|
||||
|
||||
// Compute accepts a scope in which parameter values are sourced.
|
||||
// These sources are iterated in a hierarchical fashion to determine
|
||||
// the runtime parameter values for schemas provided.
|
||||
func Compute(ctx context.Context, db database.Store, scope ComputeScope, options *ComputeOptions) ([]ComputedValue, error) {
|
||||
if options == nil {
|
||||
options = &ComputeOptions{}
|
||||
}
|
||||
compute := &compute{
|
||||
options: options,
|
||||
db: db,
|
||||
computedParameterByName: map[string]ComputedValue{},
|
||||
parameterSchemasByName: map[string]database.ParameterSchema{},
|
||||
}
|
||||
|
||||
// All parameters for the import job ID!
|
||||
parameterSchemas, err := db.GetParameterSchemasByJobID(ctx, scope.ProjectImportJobID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get project parameters: %w", err)
|
||||
}
|
||||
for _, parameterSchema := range parameterSchemas {
|
||||
compute.parameterSchemasByName[parameterSchema.Name] = parameterSchema
|
||||
}
|
||||
|
||||
// Organization parameters come first!
|
||||
err = compute.injectScope(ctx, database.GetParameterValuesByScopeParams{
|
||||
Scope: database.ParameterScopeOrganization,
|
||||
ScopeID: scope.OrganizationID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Job parameters come second!
|
||||
err = compute.injectScope(ctx, database.GetParameterValuesByScopeParams{
|
||||
Scope: database.ParameterScopeImportJob,
|
||||
ScopeID: scope.ProjectImportJobID.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Default project parameter values come second!
|
||||
for _, parameterSchema := range parameterSchemas {
|
||||
if parameterSchema.DefaultSourceScheme == database.ParameterSourceSchemeNone {
|
||||
continue
|
||||
}
|
||||
if _, ok := compute.computedParameterByName[parameterSchema.Name]; ok {
|
||||
// We already have a value! No need to use the default.
|
||||
continue
|
||||
}
|
||||
|
||||
switch parameterSchema.DefaultSourceScheme {
|
||||
case database.ParameterSourceSchemeData:
|
||||
// Inject a default value scoped to the import job ID.
|
||||
// This doesn't need to be inserted into the database,
|
||||
// because it's a dynamic value associated with the schema.
|
||||
err = compute.injectSingle(database.ParameterValue{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
Name: parameterSchema.Name,
|
||||
DestinationScheme: parameterSchema.DefaultDestinationScheme,
|
||||
SourceValue: parameterSchema.DefaultSourceValue,
|
||||
Scope: database.ParameterScopeImportJob,
|
||||
ScopeID: scope.ProjectImportJobID.String(),
|
||||
}, true)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert default value: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, xerrors.Errorf("unsupported source scheme for project version parameter %q: %q", parameterSchema.Name, string(parameterSchema.DefaultSourceScheme))
|
||||
}
|
||||
}
|
||||
|
||||
if scope.ProjectID.Valid {
|
||||
// Project parameters come third!
|
||||
err = compute.injectScope(ctx, database.GetParameterValuesByScopeParams{
|
||||
Scope: database.ParameterScopeProject,
|
||||
ScopeID: scope.ProjectID.UUID.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// User parameters come fourth!
|
||||
err = compute.injectScope(ctx, database.GetParameterValuesByScopeParams{
|
||||
Scope: database.ParameterScopeUser,
|
||||
ScopeID: scope.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if scope.WorkspaceID.Valid {
|
||||
// Workspace parameters come last!
|
||||
err = compute.injectScope(ctx, database.GetParameterValuesByScopeParams{
|
||||
Scope: database.ParameterScopeWorkspace,
|
||||
ScopeID: scope.WorkspaceID.UUID.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
values := make([]ComputedValue, 0, len(compute.computedParameterByName))
|
||||
for _, value := range compute.computedParameterByName {
|
||||
values = append(values, value)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
type compute struct {
|
||||
options *ComputeOptions
|
||||
db database.Store
|
||||
computedParameterByName map[string]ComputedValue
|
||||
parameterSchemasByName map[string]database.ParameterSchema
|
||||
}
|
||||
|
||||
// Validates and computes the value for parameters; setting the value on "parameterByName".
|
||||
func (c *compute) injectScope(ctx context.Context, scopeParams database.GetParameterValuesByScopeParams) error {
|
||||
scopedParameters, err := c.db.GetParameterValuesByScope(ctx, scopeParams)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get %s parameters: %w", scopeParams.Scope, err)
|
||||
}
|
||||
|
||||
for _, scopedParameter := range scopedParameters {
|
||||
err = c.injectSingle(scopedParameter, false)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("inject single %q: %w", scopedParameter.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compute) injectSingle(scopedParameter database.ParameterValue, defaultValue bool) error {
|
||||
parameterSchema, hasParameterSchema := c.parameterSchemasByName[scopedParameter.Name]
|
||||
if !hasParameterSchema {
|
||||
// Don't inject parameters that aren't defined by the project.
|
||||
return nil
|
||||
}
|
||||
|
||||
_, hasParameterValue := c.computedParameterByName[scopedParameter.Name]
|
||||
if hasParameterValue {
|
||||
if !parameterSchema.AllowOverrideSource &&
|
||||
// Users and workspaces cannot override anything on a project!
|
||||
(scopedParameter.Scope == database.ParameterScopeUser ||
|
||||
scopedParameter.Scope == database.ParameterScopeWorkspace) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch scopedParameter.SourceScheme {
|
||||
case database.ParameterSourceSchemeData:
|
||||
value := ComputedValue{
|
||||
ParameterValue: scopedParameter,
|
||||
SchemaID: parameterSchema.ID,
|
||||
DefaultSourceValue: defaultValue,
|
||||
}
|
||||
if c.options.HideRedisplayValues && !parameterSchema.RedisplayValue {
|
||||
value.SourceValue = ""
|
||||
}
|
||||
c.computedParameterByName[scopedParameter.Name] = value
|
||||
default:
|
||||
return xerrors.Errorf("unsupported source scheme: %q", string(parameterSchema.DefaultSourceScheme))
|
||||
}
|
||||
return nil
|
||||
}
|
222
coderd/parameter/compute_test.go
Normal file
222
coderd/parameter/compute_test.go
Normal file
@ -0,0 +1,222 @@
|
||||
package parameter_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/parameter"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/database/databasefake"
|
||||
)
|
||||
|
||||
func TestCompute(t *testing.T) {
|
||||
t.Parallel()
|
||||
generateScope := func() parameter.ComputeScope {
|
||||
return parameter.ComputeScope{
|
||||
ProjectImportJobID: uuid.New(),
|
||||
OrganizationID: uuid.NewString(),
|
||||
ProjectID: uuid.NullUUID{
|
||||
UUID: uuid.New(),
|
||||
Valid: true,
|
||||
},
|
||||
WorkspaceID: uuid.NullUUID{
|
||||
UUID: uuid.New(),
|
||||
Valid: true,
|
||||
},
|
||||
UserID: uuid.NewString(),
|
||||
}
|
||||
}
|
||||
type parameterOptions struct {
|
||||
AllowOverrideSource bool
|
||||
AllowOverrideDestination bool
|
||||
DefaultDestinationScheme database.ParameterDestinationScheme
|
||||
ProjectImportJobID uuid.UUID
|
||||
}
|
||||
generateParameter := func(t *testing.T, db database.Store, opts parameterOptions) database.ParameterSchema {
|
||||
if opts.DefaultDestinationScheme == "" {
|
||||
opts.DefaultDestinationScheme = database.ParameterDestinationSchemeEnvironmentVariable
|
||||
}
|
||||
name, err := cryptorand.String(8)
|
||||
require.NoError(t, err)
|
||||
sourceValue, err := cryptorand.String(8)
|
||||
require.NoError(t, err)
|
||||
param, err := db.InsertParameterSchema(context.Background(), database.InsertParameterSchemaParams{
|
||||
ID: uuid.New(),
|
||||
Name: name,
|
||||
JobID: opts.ProjectImportJobID,
|
||||
DefaultSourceScheme: database.ParameterSourceSchemeData,
|
||||
DefaultSourceValue: sourceValue,
|
||||
AllowOverrideSource: opts.AllowOverrideSource,
|
||||
AllowOverrideDestination: opts.AllowOverrideDestination,
|
||||
DefaultDestinationScheme: opts.DefaultDestinationScheme,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return param
|
||||
}
|
||||
|
||||
t.Run("NoValue", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
scope := generateScope()
|
||||
_, err := db.InsertParameterSchema(context.Background(), database.InsertParameterSchemaParams{
|
||||
ID: uuid.New(),
|
||||
JobID: scope.ProjectImportJobID,
|
||||
Name: "hey",
|
||||
DefaultSourceScheme: database.ParameterSourceSchemeNone,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
computed, err := parameter.Compute(context.Background(), db, scope, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, computed, 0)
|
||||
})
|
||||
|
||||
t.Run("UseDefaultProjectValue", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
scope := generateScope()
|
||||
parameterSchema := generateParameter(t, db, parameterOptions{
|
||||
ProjectImportJobID: scope.ProjectImportJobID,
|
||||
DefaultDestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
|
||||
})
|
||||
computed, err := parameter.Compute(context.Background(), db, scope, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, computed, 1)
|
||||
computedValue := computed[0]
|
||||
require.True(t, computedValue.DefaultSourceValue)
|
||||
require.Equal(t, database.ParameterScopeImportJob, computedValue.Scope)
|
||||
require.Equal(t, scope.ProjectImportJobID.String(), computedValue.ScopeID)
|
||||
require.Equal(t, computedValue.SourceValue, parameterSchema.DefaultSourceValue)
|
||||
})
|
||||
|
||||
t.Run("OverrideOrganizationWithImportJob", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
scope := generateScope()
|
||||
parameterSchema := generateParameter(t, db, parameterOptions{
|
||||
ProjectImportJobID: scope.ProjectImportJobID,
|
||||
})
|
||||
_, err := db.InsertParameterValue(context.Background(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterSchema.Name,
|
||||
Scope: database.ParameterScopeOrganization,
|
||||
ScopeID: scope.OrganizationID,
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
SourceValue: "firstnop",
|
||||
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
value, err := db.InsertParameterValue(context.Background(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterSchema.Name,
|
||||
Scope: database.ParameterScopeImportJob,
|
||||
ScopeID: scope.ProjectImportJobID.String(),
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
SourceValue: "secondnop",
|
||||
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
computed, err := parameter.Compute(context.Background(), db, scope, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, computed, 1)
|
||||
require.Equal(t, false, computed[0].DefaultSourceValue)
|
||||
require.Equal(t, value.SourceValue, computed[0].SourceValue)
|
||||
})
|
||||
|
||||
t.Run("ProjectOverridesProjectDefault", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
scope := generateScope()
|
||||
parameterSchema := generateParameter(t, db, parameterOptions{
|
||||
ProjectImportJobID: scope.ProjectImportJobID,
|
||||
})
|
||||
value, err := db.InsertParameterValue(context.Background(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterSchema.Name,
|
||||
Scope: database.ParameterScopeProject,
|
||||
ScopeID: scope.ProjectID.UUID.String(),
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
SourceValue: "nop",
|
||||
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
computed, err := parameter.Compute(context.Background(), db, scope, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, computed, 1)
|
||||
require.Equal(t, false, computed[0].DefaultSourceValue)
|
||||
require.Equal(t, value.SourceValue, computed[0].SourceValue)
|
||||
})
|
||||
|
||||
t.Run("WorkspaceCannotOverwriteProjectDefault", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
scope := generateScope()
|
||||
parameterSchema := generateParameter(t, db, parameterOptions{
|
||||
ProjectImportJobID: scope.ProjectImportJobID,
|
||||
})
|
||||
_, err := db.InsertParameterValue(context.Background(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterSchema.Name,
|
||||
Scope: database.ParameterScopeWorkspace,
|
||||
ScopeID: scope.WorkspaceID.UUID.String(),
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
SourceValue: "nop",
|
||||
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
computed, err := parameter.Compute(context.Background(), db, scope, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, computed, 1)
|
||||
require.Equal(t, true, computed[0].DefaultSourceValue)
|
||||
})
|
||||
|
||||
t.Run("WorkspaceOverwriteProjectDefault", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
scope := generateScope()
|
||||
parameterSchema := generateParameter(t, db, parameterOptions{
|
||||
AllowOverrideSource: true,
|
||||
ProjectImportJobID: scope.ProjectImportJobID,
|
||||
})
|
||||
_, err := db.InsertParameterValue(context.Background(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterSchema.Name,
|
||||
Scope: database.ParameterScopeWorkspace,
|
||||
ScopeID: scope.WorkspaceID.UUID.String(),
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
SourceValue: "nop",
|
||||
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
computed, err := parameter.Compute(context.Background(), db, scope, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, computed, 1)
|
||||
require.Equal(t, false, computed[0].DefaultSourceValue)
|
||||
})
|
||||
|
||||
t.Run("HideRedisplay", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
scope := generateScope()
|
||||
_ = generateParameter(t, db, parameterOptions{
|
||||
ProjectImportJobID: scope.ProjectImportJobID,
|
||||
DefaultDestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
|
||||
})
|
||||
computed, err := parameter.Compute(context.Background(), db, scope, ¶meter.ComputeOptions{
|
||||
HideRedisplayValues: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, computed, 1)
|
||||
computedValue := computed[0]
|
||||
require.True(t, computedValue.DefaultSourceValue)
|
||||
require.Equal(t, computedValue.SourceValue, "")
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user