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"
"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 (
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
)
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
}
2023-01-23 14:01:22 +00:00
buildParams , err := prepWorkspaceBuild ( cmd , client , prepWorkspaceBuildArgs {
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 {
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 {
2023-01-23 14:01:22 +00:00
TemplateID : template . ID ,
Name : workspaceName ,
AutostartSchedule : schedSpec ,
TTLMillis : ptr . Ref ( stopAfter . Milliseconds ( ) ) ,
ParameterValues : buildParams . parameters ,
RichParameterValues : buildParams . richParameters ,
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." )
2023-01-23 14:01:22 +00:00
cliflag . StringVarP ( cmd . Flags ( ) , & richParameterFile , "rich-parameter-file" , "" , "CODER_RICH_PARAMETER_FILE" , "" , "Specify a file path with values for rich parameters defined in the template." )
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 {
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.
func prepWorkspaceBuild ( cmd * cobra . Command , client * codersdk . Client , args prepWorkspaceBuildArgs ) ( * buildParameters , error ) {
2022-06-27 16:19:10 +00:00
ctx := cmd . 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
_ , _ = 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
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 {
_ , _ = 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
}
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 {
_ , _ = fmt . Fprintln ( cmd . OutOrStdout ( ) )
}
// Rich parameters
templateVersionParameters , err := client . TemplateVersionRichParameters ( cmd . Context ( ) , templateVersion . ID )
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
_ , _ = fmt . Fprintln ( cmd . OutOrStdout ( ) , cliui . Styles . Paragraph . Render ( "Attempting to read the variables from the rich parameter file." ) + "\r\n" )
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 {
_ , _ = 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 . 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 {
_ , _ = fmt . Fprintln ( cmd . OutOrStdout ( ) , cliui . Styles . Warn . Render ( fmt . Sprintf ( ` Parameter %q is not mutable, so can't be customized after workspace creation. ` , templateVersionParameter . Name ) ) )
continue
}
parameterValue , err := getWorkspaceBuildParameterValueFromMapOrInput ( cmd , parameterMapFromFile , templateVersionParameter )
if err != nil {
return nil , err
}
richParameters = append ( richParameters , * parameterValue )
}
if disclaimerPrinted {
_ , _ = fmt . Fprintln ( cmd . OutOrStdout ( ) )
}
2022-06-27 16:19:10 +00:00
2023-02-27 16:18:19 +00:00
err = cliui . GitAuth ( ctx , cmd . OutOrStdout ( ) , cliui . GitAuthOptions {
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
dryRun , err := client . CreateTemplateVersionDryRun ( cmd . 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 )
}
_ , _ = 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
}
2023-01-23 14:01:22 +00:00
return & buildParameters {
parameters : legacyParameters ,
richParameters : richParameters ,
} , nil
2022-06-27 16:19:10 +00:00
}