2022-02-12 19:34:04 +00:00
package cli
import (
"fmt"
2022-09-22 18:26:05 +00:00
"io"
2022-02-12 19:34:04 +00:00
"time"
"github.com/spf13/cobra"
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"
2022-04-11 23:54:30 +00:00
"github.com/coder/coder/cli/cliflag"
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
)
2022-05-02 16:08:52 +00:00
func create ( ) * cobra . Command {
2022-03-22 19:17:50 +00:00
var (
2022-06-17 20:38:10 +00:00
parameterFile string
templateName string
startAt string
stopAfter time . Duration
workspaceName string
2022-03-22 19:17:50 +00:00
)
2022-02-12 19:34:04 +00:00
cmd := & cobra . Command {
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" ,
2022-02-12 19:34:04 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2022-08-23 20:55:39 +00:00
client , err := CreateClient ( cmd )
2022-02-12 19:34:04 +00:00
if err != nil {
return err
}
2022-04-14 17:23:20 +00:00
2022-10-27 21:49:35 +00:00
organization , err := CurrentOrganization ( cmd , client )
2022-02-12 19:34:04 +00:00
if err != nil {
return err
}
2022-04-11 23:54:30 +00:00
if len ( args ) >= 1 {
2022-04-14 17:23:20 +00:00
workspaceName = args [ 0 ]
}
if workspaceName == "" {
workspaceName , err = cliui . Prompt ( cmd , cliui . PromptOptions {
Text : "Specify a name for your workspace:" ,
Validate : func ( workspaceName string ) error {
2022-06-10 14:58:42 +00:00
_ , err = client . WorkspaceByOwnerAndName ( cmd . 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
}
}
2022-06-10 14:58:42 +00:00
_ , err = client . WorkspaceByOwnerAndName ( cmd . 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 == "" {
2022-04-11 23:54:30 +00:00
_ , _ = fmt . Fprintln ( cmd . OutOrStdout ( ) , cliui . Styles . Wrap . Render ( "Select a template below to preview the provisioned infrastructure:" ) )
2022-03-22 19:17:50 +00:00
2022-04-06 17:42:40 +00:00
templates , err := client . TemplatesByOrganization ( cmd . 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!
option , err := cliui . Select ( cmd , 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 {
2022-04-06 17:42:40 +00:00
template , err = client . TemplateByName ( cmd . 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
}
2022-06-27 16:19:10 +00:00
parameters , err := prepWorkspaceBuild ( cmd , client , prepWorkspaceBuildArgs {
Template : template ,
ExistingParams : [ ] codersdk . Parameter { } ,
ParameterFile : parameterFile ,
NewWorkspaceName : workspaceName ,
2022-04-11 23:54:30 +00:00
} )
2022-02-12 19:34:04 +00:00
if err != nil {
return err
}
2022-03-22 19:17:50 +00:00
_ , err = cliui . Prompt ( cmd , 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
}
2022-09-24 01:17:10 +00:00
workspace , err := client . CreateWorkspace ( cmd . Context ( ) , organization . ID , codersdk . Me , codersdk . CreateWorkspaceRequest {
2022-05-23 22:31:41 +00:00
TemplateID : template . ID ,
Name : workspaceName ,
2022-06-07 12:37:45 +00:00
AutostartSchedule : schedSpec ,
2022-06-17 20:38:10 +00:00
TTLMillis : ptr . Ref ( stopAfter . Milliseconds ( ) ) ,
2022-05-23 22:31:41 +00:00
ParameterValues : parameters ,
2022-02-12 19:34:04 +00:00
} )
if err != nil {
return err
}
2022-04-14 17:23:20 +00:00
2022-11-07 02:50:34 +00:00
err = cliui . WorkspaceBuild ( cmd . Context ( ) , cmd . OutOrStdout ( ) , client , workspace . LatestBuild . ID )
2022-04-11 23:54:30 +00:00
if err != nil {
return err
}
2022-04-14 17:23:20 +00:00
2022-07-08 19:27:56 +00:00
_ , _ = fmt . Fprintf ( cmd . OutOrStdout ( ) , "\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
} ,
}
2022-05-20 15:59:04 +00:00
cliui . AllowSkipPrompt ( cmd )
2022-04-14 17:23:20 +00:00
cliflag . StringVarP ( cmd . Flags ( ) , & templateName , "template" , "t" , "CODER_TEMPLATE_NAME" , "" , "Specify a template name." )
2022-05-20 15:29:10 +00:00
cliflag . StringVarP ( cmd . Flags ( ) , & parameterFile , "parameter-file" , "" , "CODER_PARAMETER_FILE" , "" , "Specify a file path with parameter values." )
2022-06-17 20:38:10 +00:00
cliflag . StringVarP ( cmd . Flags ( ) , & startAt , "start-at" , "" , "CODER_WORKSPACE_START_AT" , "" , "Specify the workspace autostart schedule. Check `coder schedule start --help` for the syntax." )
cliflag . DurationVarP ( cmd . Flags ( ) , & stopAfter , "stop-after" , "" , "CODER_WORKSPACE_STOP_AFTER" , 8 * time . Hour , "Specify a duration after which the workspace should shut down (e.g. 8h)." )
2022-02-12 19:34:04 +00:00
return cmd
}
2022-06-27 16:19:10 +00:00
type prepWorkspaceBuildArgs struct {
Template codersdk . Template
ExistingParams [ ] codersdk . Parameter
ParameterFile string
NewWorkspaceName string
}
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
// Any missing params will be prompted to the user.
func prepWorkspaceBuild ( cmd * cobra . Command , client * codersdk . Client , args prepWorkspaceBuildArgs ) ( [ ] codersdk . CreateParameterRequest , error ) {
ctx := cmd . Context ( )
templateVersion , err := client . TemplateVersion ( ctx , args . Template . ActiveVersionID )
if err != nil {
return nil , err
}
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
_ , _ = fmt . Fprintln ( cmd . OutOrStdout ( ) , cliui . Styles . Paragraph . Render ( "Attempting to read the variables from the parameter file." ) + "\r\n" )
parameterMapFromFile , err = createParameterMapFromFile ( args . ParameterFile )
if err != nil {
return nil , err
}
}
disclaimerPrinted := false
parameters := make ( [ ] codersdk . CreateParameterRequest , 0 )
PromptParamLoop :
for _ , parameterSchema := range parameterSchemas {
if ! parameterSchema . AllowOverrideSource {
continue
}
if ! disclaimerPrinted {
_ , _ = fmt . Fprintln ( cmd . OutOrStdout ( ) , 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" )
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
}
}
}
parameterValue , err := getParameterValueFromMapOrInput ( cmd , parameterMapFromFile , parameterSchema )
if err != nil {
return nil , err
}
parameters = append ( parameters , codersdk . CreateParameterRequest {
Name : parameterSchema . Name ,
SourceValue : parameterValue ,
SourceScheme : codersdk . ParameterSourceSchemeData ,
DestinationScheme : parameterSchema . DefaultDestinationScheme ,
} )
}
_ , _ = fmt . Fprintln ( cmd . OutOrStdout ( ) )
// Run a dry-run with the given parameters to check correctness
dryRun , err := client . CreateTemplateVersionDryRun ( cmd . Context ( ) , templateVersion . ID , codersdk . CreateTemplateVersionDryRunRequest {
WorkspaceName : args . NewWorkspaceName ,
ParameterValues : parameters ,
} )
if err != nil {
return nil , xerrors . Errorf ( "begin workspace dry-run: %w" , err )
}
_ , _ = fmt . Fprintln ( cmd . OutOrStdout ( ) , "Planning workspace..." )
err = cliui . ProvisionerJob ( cmd . Context ( ) , cmd . OutOrStdout ( ) , cliui . ProvisionerJobOptions {
Fetch : func ( ) ( codersdk . ProvisionerJob , error ) {
return client . TemplateVersionDryRun ( cmd . Context ( ) , templateVersion . ID , dryRun . ID )
} ,
Cancel : func ( ) error {
return client . CancelTemplateVersionDryRun ( cmd . Context ( ) , templateVersion . ID , dryRun . ID )
} ,
2022-09-22 18:26:05 +00:00
Logs : func ( ) ( <- chan codersdk . ProvisionerJobLog , io . Closer , error ) {
2022-11-07 02:50:34 +00:00
return client . TemplateVersionDryRunLogsAfter ( cmd . 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 )
}
resources , err := client . TemplateVersionDryRunResources ( cmd . Context ( ) , templateVersion . ID , dryRun . ID )
if err != nil {
return nil , xerrors . Errorf ( "get workspace dry-run resources: %w" , err )
}
err = cliui . WorkspaceResources ( cmd . OutOrStdout ( ) , resources , cliui . WorkspaceResourcesOptions {
WorkspaceName : args . NewWorkspaceName ,
// Since agents haven't connected yet, hiding this makes more sense.
HideAgentState : true ,
Title : "Workspace Preview" ,
} )
if err != nil {
return nil , err
}
return parameters , nil
}