2022-02-12 19:34:04 +00:00
package cli
import (
2023-02-27 16:18:19 +00:00
"context"
2022-02-12 19:34:04 +00:00
"fmt"
2022-09-22 18:26:05 +00:00
"io"
2022-02-12 19:34:04 +00:00
"time"
2022-04-14 17:23:20 +00:00
"golang.org/x/exp/slices"
2022-02-12 19:34:04 +00:00
"golang.org/x/xerrors"
2023-03-23 22:42:20 +00:00
"github.com/coder/coder/cli/clibase"
2022-03-22 19:17:50 +00:00
"github.com/coder/coder/cli/cliui"
2022-06-02 10:23:34 +00:00
"github.com/coder/coder/coderd/util/ptr"
2022-03-22 19:17:50 +00:00
"github.com/coder/coder/codersdk"
2022-02-12 19:34:04 +00:00
)
2023-03-23 22:42:20 +00:00
func ( r * RootCmd ) create ( ) * clibase . Cmd {
2022-03-22 19:17:50 +00:00
var (
2023-01-23 14:01:22 +00:00
parameterFile string
richParameterFile string
templateName string
startAt string
stopAfter time . Duration
workspaceName string
2022-03-22 19:17:50 +00:00
)
2023-03-23 22:42:20 +00:00
client := new ( codersdk . Client )
cmd := & clibase . Cmd {
2022-05-09 22:42:02 +00:00
Annotations : workspaceCommand ,
Use : "create [name]" ,
2022-09-19 16:36:18 +00:00
Short : "Create a workspace" ,
2023-03-23 22:42:20 +00:00
Middleware : clibase . Chain ( r . InitClient ( client ) ) ,
Handler : func ( inv * clibase . Invocation ) error {
organization , err := CurrentOrganization ( inv , client )
2022-02-12 19:34:04 +00:00
if err != nil {
return err
}
2022-04-14 17:23:20 +00:00
2023-03-23 22:42:20 +00:00
if len ( inv . Args ) >= 1 {
workspaceName = inv . Args [ 0 ]
2022-04-14 17:23:20 +00:00
}
if workspaceName == "" {
2023-03-23 22:42:20 +00:00
workspaceName , err = cliui . Prompt ( inv , cliui . PromptOptions {
2022-04-14 17:23:20 +00:00
Text : "Specify a name for your workspace:" ,
Validate : func ( workspaceName string ) error {
2023-03-23 22:42:20 +00:00
_ , err = client . WorkspaceByOwnerAndName ( inv . Context ( ) , codersdk . Me , workspaceName , codersdk . WorkspaceOptions { } )
2022-04-14 17:23:20 +00:00
if err == nil {
return xerrors . Errorf ( "A workspace already exists named %q!" , workspaceName )
}
return nil
} ,
} )
if err != nil {
return err
}
}
2023-03-23 22:42:20 +00:00
_ , err = client . WorkspaceByOwnerAndName ( inv . Context ( ) , codersdk . Me , workspaceName , codersdk . WorkspaceOptions { } )
2022-04-14 17:23:20 +00:00
if err == nil {
return xerrors . Errorf ( "A workspace already exists named %q!" , workspaceName )
2022-04-11 23:54:30 +00:00
}
2022-04-06 17:42:40 +00:00
var template codersdk . Template
if templateName == "" {
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout , cliui . Styles . Wrap . Render ( "Select a template below to preview the provisioned infrastructure:" ) )
2022-03-22 19:17:50 +00:00
2023-03-23 22:42:20 +00:00
templates , err := client . TemplatesByOrganization ( inv . Context ( ) , organization . ID )
2022-03-22 19:17:50 +00:00
if err != nil {
return err
}
2022-04-14 17:23:20 +00:00
slices . SortFunc ( templates , func ( a , b codersdk . Template ) bool {
2022-09-09 19:30:31 +00:00
return a . ActiveUserCount > b . ActiveUserCount
2022-04-14 17:23:20 +00:00
} )
templateNames := make ( [ ] string , 0 , len ( templates ) )
templateByName := make ( map [ string ] codersdk . Template , len ( templates ) )
2022-04-06 17:42:40 +00:00
for _ , template := range templates {
2022-04-11 23:54:30 +00:00
templateName := template . Name
2022-04-14 17:23:20 +00:00
2022-09-09 19:30:31 +00:00
if template . ActiveUserCount > 0 {
templateName += cliui . Styles . Placeholder . Render (
fmt . Sprintf (
" (used by %s)" ,
formatActiveDevelopers ( template . ActiveUserCount ) ,
) ,
)
2022-04-11 23:54:30 +00:00
}
2022-04-14 17:23:20 +00:00
2022-04-11 23:54:30 +00:00
templateNames = append ( templateNames , templateName )
templateByName [ templateName ] = template
2022-03-22 19:17:50 +00:00
}
2022-04-14 17:23:20 +00:00
2022-03-22 19:17:50 +00:00
// Move the cursor up a single line for nicer display!
2023-03-23 22:42:20 +00:00
option , err := cliui . Select ( inv , cliui . SelectOptions {
2022-04-06 17:42:40 +00:00
Options : templateNames ,
2022-03-22 19:17:50 +00:00
HideSearch : true ,
2022-02-12 19:34:04 +00:00
} )
if err != nil {
2022-03-22 19:17:50 +00:00
return err
}
2022-04-14 17:23:20 +00:00
2022-04-06 17:42:40 +00:00
template = templateByName [ option ]
2022-03-22 19:17:50 +00:00
} else {
2023-03-23 22:42:20 +00:00
template , err = client . TemplateByName ( inv . Context ( ) , organization . ID , templateName )
2022-03-22 19:17:50 +00:00
if err != nil {
2022-04-06 17:42:40 +00:00
return xerrors . Errorf ( "get template by name: %w" , err )
2022-03-22 19:17:50 +00:00
}
2022-02-12 19:34:04 +00:00
}
2022-03-22 19:17:50 +00:00
2022-06-17 20:38:10 +00:00
var schedSpec * string
if startAt != "" {
sched , err := parseCLISchedule ( startAt )
if err != nil {
return err
}
schedSpec = ptr . Ref ( sched . String ( ) )
2022-06-07 12:37:45 +00:00
}
2023-03-23 22:42:20 +00:00
buildParams , err := prepWorkspaceBuild ( inv , client , prepWorkspaceBuildArgs {
2023-01-23 14:01:22 +00:00
Template : template ,
ExistingParams : [ ] codersdk . Parameter { } ,
ParameterFile : parameterFile ,
RichParameterFile : richParameterFile ,
NewWorkspaceName : workspaceName ,
2022-04-11 23:54:30 +00:00
} )
2022-02-12 19:34:04 +00:00
if err != nil {
2023-03-23 22:42:20 +00:00
return xerrors . Errorf ( "prepare build: %w" , err )
2022-02-12 19:34:04 +00:00
}
2023-03-23 22:42:20 +00:00
_ , err = cliui . Prompt ( inv , cliui . PromptOptions {
2022-04-11 23:54:30 +00:00
Text : "Confirm create?" ,
2022-02-12 19:34:04 +00:00
IsConfirm : true ,
} )
if err != nil {
return err
}
2023-03-17 17:14:46 +00:00
var ttlMillis * int64
if stopAfter > 0 {
ttlMillis = ptr . Ref ( stopAfter . Milliseconds ( ) )
} else if template . MaxTTLMillis > 0 {
ttlMillis = & template . MaxTTLMillis
}
2023-03-23 22:42:20 +00:00
workspace , err := client . CreateWorkspace ( inv . Context ( ) , organization . ID , codersdk . Me , codersdk . CreateWorkspaceRequest {
2023-01-23 14:01:22 +00:00
TemplateID : template . ID ,
Name : workspaceName ,
AutostartSchedule : schedSpec ,
2023-03-17 17:14:46 +00:00
TTLMillis : ttlMillis ,
2023-01-23 14:01:22 +00:00
ParameterValues : buildParams . parameters ,
RichParameterValues : buildParams . richParameters ,
2022-02-12 19:34:04 +00:00
} )
if err != nil {
2023-03-23 22:42:20 +00:00
return xerrors . Errorf ( "create workspace: %w" , err )
2022-02-12 19:34:04 +00:00
}
2022-04-14 17:23:20 +00:00
2023-03-23 22:42:20 +00:00
err = cliui . WorkspaceBuild ( inv . Context ( ) , inv . Stdout , client , workspace . LatestBuild . ID )
2022-04-11 23:54:30 +00:00
if err != nil {
2023-03-23 22:42:20 +00:00
return xerrors . Errorf ( "watch build: %w" , err )
2022-04-11 23:54:30 +00:00
}
2022-04-14 17:23:20 +00:00
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintf ( inv . Stdout , "\nThe %s workspace has been created at %s!\n" , cliui . Styles . Keyword . Render ( workspace . Name ) , cliui . Styles . DateTimeStamp . Render ( time . Now ( ) . Format ( time . Stamp ) ) )
2022-04-11 23:54:30 +00:00
return nil
2022-02-12 19:34:04 +00:00
} ,
}
2023-03-23 22:42:20 +00:00
cmd . Options = append ( cmd . Options ,
clibase . Option {
Flag : "template" ,
FlagShorthand : "t" ,
Env : "CODER_TEMPLATE_NAME" ,
Description : "Specify a template name." ,
Value : clibase . StringOf ( & templateName ) ,
} ,
clibase . Option {
Flag : "parameter-file" ,
Env : "CODER_PARAMETER_FILE" ,
Description : "Specify a file path with parameter values." ,
Value : clibase . StringOf ( & parameterFile ) ,
} ,
clibase . Option {
Flag : "rich-parameter-file" ,
Env : "CODER_RICH_PARAMETER_FILE" ,
Description : "Specify a file path with values for rich parameters defined in the template." ,
Value : clibase . StringOf ( & richParameterFile ) ,
} ,
clibase . Option {
Flag : "start-at" ,
Env : "CODER_WORKSPACE_START_AT" ,
Description : "Specify the workspace autostart schedule. Check coder schedule start --help for the syntax." ,
Value : clibase . StringOf ( & startAt ) ,
} ,
clibase . Option {
Flag : "stop-after" ,
Env : "CODER_WORKSPACE_STOP_AFTER" ,
Description : "Specify a duration after which the workspace should shut down (e.g. 8h)." ,
Value : clibase . DurationOf ( & stopAfter ) ,
} ,
cliui . SkipPromptOption ( ) ,
)
2022-02-12 19:34:04 +00:00
return cmd
}
2022-06-27 16:19:10 +00:00
type prepWorkspaceBuildArgs struct {
2023-01-23 14:01:22 +00:00
Template codersdk . Template
ExistingParams [ ] codersdk . Parameter
ParameterFile string
ExistingRichParams [ ] codersdk . WorkspaceBuildParameter
RichParameterFile string
NewWorkspaceName string
UpdateWorkspace bool
}
type buildParameters struct {
// Parameters contains legacy parameters stored in /parameters.
parameters [ ] codersdk . CreateParameterRequest
// Rich parameters stores values for build parameters annotated with description, icon, type, etc.
richParameters [ ] codersdk . WorkspaceBuildParameter
2022-06-27 16:19:10 +00:00
}
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
2023-01-23 14:01:22 +00:00
// Any missing params will be prompted to the user. It supports legacy and rich parameters.
2023-03-23 22:42:20 +00:00
func prepWorkspaceBuild ( inv * clibase . Invocation , client * codersdk . Client , args prepWorkspaceBuildArgs ) ( * buildParameters , error ) {
ctx := inv . Context ( )
2023-02-02 15:44:57 +00:00
var useRichParameters bool
if len ( args . ExistingRichParams ) > 0 && len ( args . RichParameterFile ) > 0 {
useRichParameters = true
}
var useLegacyParameters bool
if len ( args . ExistingParams ) > 0 || len ( args . ParameterFile ) > 0 {
useLegacyParameters = true
}
if useRichParameters && useLegacyParameters {
return nil , xerrors . Errorf ( "Rich parameters can't be used together with legacy parameters." )
}
2022-06-27 16:19:10 +00:00
templateVersion , err := client . TemplateVersion ( ctx , args . Template . ActiveVersionID )
if err != nil {
return nil , err
}
2023-01-23 14:01:22 +00:00
// Legacy parameters
2022-06-27 16:19:10 +00:00
parameterSchemas , err := client . TemplateVersionSchema ( ctx , templateVersion . ID )
if err != nil {
return nil , err
}
// parameterMapFromFile can be nil if parameter file is not specified
var parameterMapFromFile map [ string ] string
useParamFile := false
if args . ParameterFile != "" {
useParamFile = true
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout , cliui . Styles . Paragraph . Render ( "Attempting to read the variables from the parameter file." ) + "\r\n" )
2022-06-27 16:19:10 +00:00
parameterMapFromFile , err = createParameterMapFromFile ( args . ParameterFile )
if err != nil {
return nil , err
}
}
disclaimerPrinted := false
2023-01-23 14:01:22 +00:00
legacyParameters := make ( [ ] codersdk . CreateParameterRequest , 0 )
2022-06-27 16:19:10 +00:00
PromptParamLoop :
for _ , parameterSchema := range parameterSchemas {
if ! parameterSchema . AllowOverrideSource {
continue
}
if ! disclaimerPrinted {
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout , cliui . Styles . Paragraph . Render ( "This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss)." ) + "\r\n" )
2022-06-27 16:19:10 +00:00
disclaimerPrinted = true
}
// Param file is all or nothing
if ! useParamFile {
for _ , e := range args . ExistingParams {
if e . Name == parameterSchema . Name {
// If the param already exists, we do not need to prompt it again.
// The workspace scope will reuse params for each build.
continue PromptParamLoop
}
}
}
2023-03-23 22:42:20 +00:00
parameterValue , err := getParameterValueFromMapOrInput ( inv , parameterMapFromFile , parameterSchema )
2022-06-27 16:19:10 +00:00
if err != nil {
return nil , err
}
2023-01-23 14:01:22 +00:00
legacyParameters = append ( legacyParameters , codersdk . CreateParameterRequest {
2022-06-27 16:19:10 +00:00
Name : parameterSchema . Name ,
SourceValue : parameterValue ,
SourceScheme : codersdk . ParameterSourceSchemeData ,
DestinationScheme : parameterSchema . DefaultDestinationScheme ,
} )
}
2023-01-23 14:01:22 +00:00
if disclaimerPrinted {
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout )
2023-01-23 14:01:22 +00:00
}
// Rich parameters
2023-03-23 22:42:20 +00:00
templateVersionParameters , err := client . TemplateVersionRichParameters ( inv . Context ( ) , templateVersion . ID )
2023-01-23 14:01:22 +00:00
if err != nil {
return nil , xerrors . Errorf ( "get template version rich parameters: %w" , err )
}
parameterMapFromFile = map [ string ] string { }
useParamFile = false
if args . RichParameterFile != "" {
useParamFile = true
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout , cliui . Styles . Paragraph . Render ( "Attempting to read the variables from the rich parameter file." ) + "\r\n" )
2023-01-23 14:01:22 +00:00
parameterMapFromFile , err = createParameterMapFromFile ( args . RichParameterFile )
if err != nil {
return nil , err
}
}
disclaimerPrinted = false
richParameters := make ( [ ] codersdk . WorkspaceBuildParameter , 0 )
PromptRichParamLoop :
for _ , templateVersionParameter := range templateVersionParameters {
if ! disclaimerPrinted {
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout , cliui . Styles . Paragraph . Render ( "This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss)." ) + "\r\n" )
2023-01-23 14:01:22 +00:00
disclaimerPrinted = true
}
// Param file is all or nothing
if ! useParamFile {
for _ , e := range args . ExistingRichParams {
if e . Name == templateVersionParameter . Name {
// If the param already exists, we do not need to prompt it again.
// The workspace scope will reuse params for each build.
continue PromptRichParamLoop
}
}
}
if args . UpdateWorkspace && ! templateVersionParameter . Mutable {
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout , cliui . Styles . Warn . Render ( fmt . Sprintf ( ` Parameter %q is not mutable, so can't be customized after workspace creation. ` , templateVersionParameter . Name ) ) )
2023-01-23 14:01:22 +00:00
continue
}
2023-03-23 22:42:20 +00:00
parameterValue , err := getWorkspaceBuildParameterValueFromMapOrInput ( inv , parameterMapFromFile , templateVersionParameter )
2023-01-23 14:01:22 +00:00
if err != nil {
return nil , err
}
richParameters = append ( richParameters , * parameterValue )
}
if disclaimerPrinted {
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout )
2023-01-23 14:01:22 +00:00
}
2022-06-27 16:19:10 +00:00
2023-03-23 22:42:20 +00:00
err = cliui . GitAuth ( ctx , inv . Stdout , cliui . GitAuthOptions {
2023-02-27 16:18:19 +00:00
Fetch : func ( ctx context . Context ) ( [ ] codersdk . TemplateVersionGitAuth , error ) {
return client . TemplateVersionGitAuth ( ctx , templateVersion . ID )
} ,
} )
if err != nil {
return nil , xerrors . Errorf ( "template version git auth: %w" , err )
}
2022-06-27 16:19:10 +00:00
// Run a dry-run with the given parameters to check correctness
2023-03-23 22:42:20 +00:00
dryRun , err := client . CreateTemplateVersionDryRun ( inv . Context ( ) , templateVersion . ID , codersdk . CreateTemplateVersionDryRunRequest {
2023-01-23 14:01:22 +00:00
WorkspaceName : args . NewWorkspaceName ,
ParameterValues : legacyParameters ,
RichParameterValues : richParameters ,
2022-06-27 16:19:10 +00:00
} )
if err != nil {
return nil , xerrors . Errorf ( "begin workspace dry-run: %w" , err )
}
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout , "Planning workspace..." )
err = cliui . ProvisionerJob ( inv . Context ( ) , inv . Stdout , cliui . ProvisionerJobOptions {
2022-06-27 16:19:10 +00:00
Fetch : func ( ) ( codersdk . ProvisionerJob , error ) {
2023-03-23 22:42:20 +00:00
return client . TemplateVersionDryRun ( inv . Context ( ) , templateVersion . ID , dryRun . ID )
2022-06-27 16:19:10 +00:00
} ,
Cancel : func ( ) error {
2023-03-23 22:42:20 +00:00
return client . CancelTemplateVersionDryRun ( inv . Context ( ) , templateVersion . ID , dryRun . ID )
2022-06-27 16:19:10 +00:00
} ,
2022-09-22 18:26:05 +00:00
Logs : func ( ) ( <- chan codersdk . ProvisionerJobLog , io . Closer , error ) {
2023-03-23 22:42:20 +00:00
return client . TemplateVersionDryRunLogsAfter ( inv . Context ( ) , templateVersion . ID , dryRun . ID , 0 )
2022-06-27 16:19:10 +00:00
} ,
// Don't show log output for the dry-run unless there's an error.
Silent : true ,
} )
if err != nil {
// TODO (Dean): reprompt for parameter values if we deem it to
// be a validation error
return nil , xerrors . Errorf ( "dry-run workspace: %w" , err )
}
2023-03-23 22:42:20 +00:00
resources , err := client . TemplateVersionDryRunResources ( inv . Context ( ) , templateVersion . ID , dryRun . ID )
2022-06-27 16:19:10 +00:00
if err != nil {
return nil , xerrors . Errorf ( "get workspace dry-run resources: %w" , err )
}
2023-03-23 22:42:20 +00:00
err = cliui . WorkspaceResources ( inv . Stdout , resources , cliui . WorkspaceResourcesOptions {
2022-06-27 16:19:10 +00:00
WorkspaceName : args . NewWorkspaceName ,
// Since agents haven't connected yet, hiding this makes more sense.
HideAgentState : true ,
Title : "Workspace Preview" ,
} )
if err != nil {
2023-03-23 22:42:20 +00:00
return nil , xerrors . Errorf ( "get resources: %w" , err )
2022-06-27 16:19:10 +00:00
}
2023-01-23 14:01:22 +00:00
return & buildParameters {
parameters : legacyParameters ,
richParameters : richParameters ,
} , nil
2022-06-27 16:19:10 +00:00
}