2022-02-10 14:33:27 +00:00
package cli
import (
2023-03-08 15:32:00 +00:00
"errors"
2022-02-10 14:33:27 +00:00
"fmt"
2022-09-22 18:26:05 +00:00
"io"
2022-02-10 14:33:27 +00:00
"os"
"path/filepath"
2022-04-11 23:54:30 +00:00
"strings"
2022-02-10 14:33:27 +00:00
"time"
2022-09-07 20:01:18 +00:00
"unicode/utf8"
2022-02-10 14:33:27 +00:00
2022-10-13 23:02:52 +00:00
"github.com/google/uuid"
2022-02-10 14:33:27 +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-03-25 21:07:45 +00:00
"github.com/coder/coder/coderd/database"
2022-06-07 12:37:45 +00:00
"github.com/coder/coder/coderd/util/ptr"
2022-02-10 14:33:27 +00:00
"github.com/coder/coder/codersdk"
2022-02-12 19:34:04 +00:00
"github.com/coder/coder/provisionerd"
2022-02-10 14:33:27 +00:00
)
2023-03-23 22:42:20 +00:00
func ( r * RootCmd ) templateCreate ( ) * clibase . Cmd {
2022-02-12 19:34:04 +00:00
var (
2022-11-16 22:34:06 +00:00
provisioner string
provisionerTags [ ] string
parameterFile string
2023-02-17 08:07:45 +00:00
variablesFile string
variables [ ] string
2022-11-16 22:34:06 +00:00
defaultTTL time . Duration
2023-01-16 20:32:11 +00:00
uploadFlags templateUploadFlags
2022-02-12 19:34:04 +00:00
)
2023-03-23 22:42:20 +00:00
client := new ( codersdk . Client )
cmd := & clibase . Cmd {
2022-04-14 17:23:20 +00:00
Use : "create [name]" ,
2022-05-12 11:54:58 +00:00
Short : "Create a template from the current directory or as specified by flag" ,
2023-03-23 22:42:20 +00:00
Middleware : clibase . Chain (
clibase . RequireRangeArgs ( 0 , 1 ) ,
r . InitClient ( client ) ,
) ,
Handler : func ( inv * clibase . Invocation ) error {
organization , err := CurrentOrganization ( inv , client )
2022-02-10 14:33:27 +00:00
if err != nil {
return err
}
2022-04-14 17:23:20 +00:00
2023-03-23 22:42:20 +00:00
templateName , err := uploadFlags . templateName ( inv . Args )
2023-01-16 20:32:11 +00:00
if err != nil {
return err
2022-03-22 19:17:50 +00:00
}
2022-04-14 17:23:20 +00:00
2022-09-07 20:01:18 +00:00
if utf8 . RuneCountInString ( templateName ) > 31 {
return xerrors . Errorf ( "Template name must be less than 32 characters" )
}
2023-03-23 22:42:20 +00:00
_ , 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 ( "A template already exists named %q!" , templateName )
2022-02-10 14:33:27 +00:00
}
2022-05-12 11:54:58 +00:00
// Confirm upload of the directory.
2023-03-23 22:42:20 +00:00
resp , err := uploadFlags . upload ( inv , client )
2022-04-11 23:54:30 +00:00
if err != nil {
return err
}
2022-11-16 22:34:06 +00:00
tags , err := ParseProvisionerTags ( provisionerTags )
if err != nil {
return err
}
2023-03-23 22:42:20 +00:00
job , _ , err := createValidTemplateVersion ( inv , createValidTemplateVersionArgs {
2022-11-16 22:34:06 +00:00
Client : client ,
Organization : organization ,
Provisioner : database . ProvisionerType ( provisioner ) ,
FileID : resp . ID ,
ParameterFile : parameterFile ,
ProvisionerTags : tags ,
2023-02-17 08:07:45 +00:00
VariablesFile : variablesFile ,
Variables : variables ,
2022-06-17 17:22:28 +00:00
} )
2022-02-10 14:33:27 +00:00
if err != nil {
return err
}
2023-01-16 20:32:11 +00:00
if ! uploadFlags . stdin ( ) {
2023-03-23 22:42:20 +00:00
_ , err = cliui . Prompt ( inv , cliui . PromptOptions {
2023-01-16 20:32:11 +00:00
Text : "Confirm create?" ,
IsConfirm : true ,
} )
if err != nil {
return err
}
2022-02-10 14:33:27 +00:00
}
2022-06-07 12:37:45 +00:00
createReq := codersdk . CreateTemplateRequest {
2022-11-09 19:36:25 +00:00
Name : templateName ,
VersionID : job . ID ,
DefaultTTLMillis : ptr . Ref ( defaultTTL . Milliseconds ( ) ) ,
2022-06-07 12:37:45 +00:00
}
2023-03-23 22:42:20 +00:00
_ , err = client . CreateTemplate ( inv . Context ( ) , organization . ID , createReq )
2022-02-10 14:33:27 +00:00
if err != nil {
2022-02-12 19:34:04 +00:00
return err
2022-02-10 14:33:27 +00:00
}
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout , "\n" + cliui . Styles . Wrap . Render (
2022-07-08 19:27:56 +00:00
"The " + cliui . Styles . Keyword . Render ( templateName ) + " template has been created at " + cliui . Styles . DateTimeStamp . Render ( time . Now ( ) . Format ( time . Stamp ) ) + "! " +
2022-04-11 23:54:30 +00:00
"Developers can provision a workspace with this template using:" ) + "\n" )
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout , " " + cliui . Styles . Code . Render ( fmt . Sprintf ( "coder create --template=%q [workspace name]" , templateName ) ) )
_ , _ = fmt . Fprintln ( inv . Stdout )
2022-04-11 23:54:30 +00:00
2022-02-10 14:33:27 +00:00
return nil
} ,
}
2023-03-23 22:42:20 +00:00
cmd . Options = clibase . OptionSet {
{
Flag : "parameter-file" ,
Description : "Specify a file path with parameter values." ,
Value : clibase . StringOf ( & parameterFile ) ,
} ,
{
Flag : "variables-file" ,
Description : "Specify a file path with values for Terraform-managed variables." ,
Value : clibase . StringOf ( & variablesFile ) ,
} ,
{
Flag : "variable" ,
Description : "Specify a set of values for Terraform-managed variables." ,
Value : clibase . StringArrayOf ( & variables ) ,
} ,
{
Flag : "provisioner-tag" ,
Description : "Specify a set of tags to target provisioner daemons." ,
Value : clibase . StringArrayOf ( & provisionerTags ) ,
} ,
{
Flag : "default-ttl" ,
Description : "Specify a default TTL for workspaces created from this template." ,
Default : "24h" ,
Value : clibase . DurationOf ( & defaultTTL ) ,
} ,
uploadFlags . option ( ) ,
{
Flag : "test.provisioner" ,
Description : "Customize the provisioner backend." ,
Default : "terraform" ,
Value : clibase . StringOf ( & provisioner ) ,
Hidden : true ,
} ,
cliui . SkipPromptOption ( ) ,
2022-02-12 19:34:04 +00:00
}
return cmd
}
2022-06-17 17:22:28 +00:00
type createValidTemplateVersionArgs struct {
2022-09-24 01:17:36 +00:00
Name string
2022-06-17 17:22:28 +00:00
Client * codersdk . Client
Organization codersdk . Organization
Provisioner database . ProvisionerType
2022-10-13 23:02:52 +00:00
FileID uuid . UUID
2022-06-17 17:22:28 +00:00
ParameterFile string
2023-02-17 08:07:45 +00:00
VariablesFile string
Variables [ ] string
2022-06-17 17:22:28 +00:00
// Template is only required if updating a template's active version.
Template * codersdk . Template
// ReuseParameters will attempt to reuse params from the Template field
// before prompting the user. Set to false to always prompt for param
// values.
ReuseParameters bool
2022-11-16 22:34:06 +00:00
ProvisionerTags map [ string ] string
2022-06-17 17:22:28 +00:00
}
2023-03-23 22:42:20 +00:00
func createValidTemplateVersion ( inv * clibase . Invocation , args createValidTemplateVersionArgs , parameters ... codersdk . CreateParameterRequest ) ( * codersdk . TemplateVersion , [ ] codersdk . CreateParameterRequest , error ) {
2022-06-17 17:22:28 +00:00
client := args . Client
2023-02-17 08:07:45 +00:00
variableValues , err := loadVariableValuesFromFile ( args . VariablesFile )
if err != nil {
return nil , nil , err
}
variableValuesFromKeyValues , err := loadVariableValuesFromOptions ( args . Variables )
2023-02-15 17:24:15 +00:00
if err != nil {
return nil , nil , err
}
2023-02-17 08:07:45 +00:00
variableValues = append ( variableValues , variableValuesFromKeyValues ... )
2023-02-15 17:24:15 +00:00
2022-06-17 17:22:28 +00:00
req := codersdk . CreateTemplateVersionRequest {
2023-02-15 17:24:15 +00:00
Name : args . Name ,
StorageMethod : codersdk . ProvisionerStorageMethodFile ,
FileID : args . FileID ,
Provisioner : codersdk . ProvisionerType ( args . Provisioner ) ,
ParameterValues : parameters ,
ProvisionerTags : args . ProvisionerTags ,
UserVariableValues : variableValues ,
2022-06-17 17:22:28 +00:00
}
if args . Template != nil {
req . TemplateID = args . Template . ID
}
2023-03-23 22:42:20 +00:00
version , err := client . CreateTemplateVersion ( inv . Context ( ) , args . Organization . ID , req )
2022-02-12 19:34:04 +00:00
if err != nil {
2022-03-22 19:17:50 +00:00
return nil , nil , err
2022-02-12 19:34:04 +00:00
}
2022-03-22 19:17:50 +00:00
2023-03-23 22:42:20 +00:00
err = cliui . ProvisionerJob ( inv . Context ( ) , inv . Stdout , cliui . ProvisionerJobOptions {
2022-03-22 19:17:50 +00:00
Fetch : func ( ) ( codersdk . ProvisionerJob , error ) {
2023-03-23 22:42:20 +00:00
version , err := client . TemplateVersion ( inv . Context ( ) , version . ID )
2022-03-22 19:17:50 +00:00
return version . Job , err
} ,
Cancel : func ( ) error {
2023-03-23 22:42:20 +00:00
return client . CancelTemplateVersion ( inv . Context ( ) , version . ID )
2022-03-22 19:17:50 +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 . TemplateVersionLogsAfter ( inv . Context ( ) , version . ID , 0 )
2022-03-22 19:17:50 +00:00
} ,
} )
2022-02-12 19:34:04 +00:00
if err != nil {
2023-03-08 15:32:00 +00:00
var jobErr * cliui . ProvisionerJobError
if errors . As ( err , & jobErr ) && ! provisionerd . IsMissingParameterErrorCode ( string ( jobErr . Code ) ) {
2022-03-28 18:43:22 +00:00
return nil , nil , err
}
2022-02-12 19:34:04 +00:00
}
2023-03-23 22:42:20 +00:00
version , err = client . TemplateVersion ( inv . Context ( ) , version . ID )
2022-02-12 19:34:04 +00:00
if err != nil {
2022-03-22 19:17:50 +00:00
return nil , nil , err
2022-02-12 19:34:04 +00:00
}
2023-03-23 22:42:20 +00:00
parameterSchemas , err := client . TemplateVersionSchema ( inv . Context ( ) , version . ID )
2022-02-12 19:34:04 +00:00
if err != nil {
2022-03-22 19:17:50 +00:00
return nil , nil , err
2022-02-12 19:34:04 +00:00
}
2023-03-23 22:42:20 +00:00
parameterValues , err := client . TemplateVersionParameters ( inv . Context ( ) , version . ID )
2022-02-12 19:34:04 +00:00
if err != nil {
2022-03-22 19:17:50 +00:00
return nil , nil , err
2022-02-12 19:34:04 +00:00
}
2022-06-17 17:22:28 +00:00
// lastParameterValues are pulled from the current active template version if
// templateID is provided. This allows pulling params from the last
// version instead of prompting if we are updating template versions.
lastParameterValues := make ( map [ string ] codersdk . Parameter )
if args . ReuseParameters && args . Template != nil {
2023-03-23 22:42:20 +00:00
activeVersion , err := client . TemplateVersion ( inv . Context ( ) , args . Template . ActiveVersionID )
2022-06-17 17:22:28 +00:00
if err != nil {
return nil , nil , xerrors . Errorf ( "Fetch current active template version: %w" , err )
}
// We don't want to compute the params, we only want to copy from this scope
2023-03-23 22:42:20 +00:00
values , err := client . Parameters ( inv . Context ( ) , codersdk . ParameterImportJob , activeVersion . Job . ID )
2022-06-17 17:22:28 +00:00
if err != nil {
return nil , nil , xerrors . Errorf ( "Fetch previous version parameters: %w" , err )
}
for _ , value := range values {
lastParameterValues [ value . Name ] = value
}
}
2023-03-08 15:32:00 +00:00
if provisionerd . IsMissingParameterErrorCode ( string ( version . Job . ErrorCode ) ) {
2022-06-17 20:20:13 +00:00
valuesBySchemaID := map [ string ] codersdk . ComputedParameter { }
2022-02-12 19:34:04 +00:00
for _ , parameterValue := range parameterValues {
valuesBySchemaID [ parameterValue . SchemaID . String ( ) ] = parameterValue
}
2022-06-17 17:22:28 +00:00
// parameterMapFromFile can be nil if parameter file is not specified
var parameterMapFromFile map [ string ] string
if args . ParameterFile != "" {
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-17 17:22:28 +00:00
parameterMapFromFile , err = createParameterMapFromFile ( args . ParameterFile )
if err != nil {
return nil , nil , err
}
}
// pulled params come from the last template version
pulled := make ( [ ] string , 0 )
2022-05-19 13:29:36 +00:00
missingSchemas := make ( [ ] codersdk . ParameterSchema , 0 )
2022-02-12 19:34:04 +00:00
for _ , parameterSchema := range parameterSchemas {
_ , ok := valuesBySchemaID [ parameterSchema . ID . String ( ) ]
if ok {
continue
}
2022-05-20 15:29:10 +00:00
2022-06-17 17:22:28 +00:00
// The file values are handled below. So don't handle them here,
// just check if a value is present in the file.
_ , fileOk := parameterMapFromFile [ parameterSchema . Name ]
if inherit , ok := lastParameterValues [ parameterSchema . Name ] ; ok && ! fileOk {
// If the value is not in the param file, and can be pulled from the last template version,
// then don't mark it as missing.
parameters = append ( parameters , codersdk . CreateParameterRequest {
CloneID : inherit . ID ,
} )
pulled = append ( pulled , fmt . Sprintf ( "%q" , parameterSchema . Name ) )
continue
2022-05-20 15:29:10 +00:00
}
2022-06-17 17:22:28 +00:00
missingSchemas = append ( missingSchemas , parameterSchema )
2022-05-20 15:29:10 +00:00
}
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout , cliui . Styles . Paragraph . Render ( "This template has required variables! They are scoped to the template, and not viewable after being set." ) )
2022-06-17 17:22:28 +00:00
if len ( pulled ) > 0 {
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout , cliui . Styles . Paragraph . Render ( fmt . Sprintf ( "The following parameter values are being pulled from the latest template version: %s." , strings . Join ( pulled , ", " ) ) ) )
_ , _ = fmt . Fprintln ( inv . Stdout , cliui . Styles . Paragraph . Render ( "Use \"--always-prompt\" flag to change the values." ) )
2022-06-17 17:22:28 +00:00
}
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprint ( inv . Stdout , "\r\n" )
2022-06-17 17:22:28 +00:00
2022-03-22 19:17:50 +00:00
for _ , parameterSchema := range missingSchemas {
2023-03-23 22:42:20 +00:00
parameterValue , err := getParameterValueFromMapOrInput ( inv , parameterMapFromFile , parameterSchema )
2022-02-12 19:34:04 +00:00
if err != nil {
2022-03-22 19:17:50 +00:00
return nil , nil , err
2022-02-12 19:34:04 +00:00
}
2022-03-22 19:17:50 +00:00
parameters = append ( parameters , codersdk . CreateParameterRequest {
2022-02-12 19:34:04 +00:00
Name : parameterSchema . Name ,
2022-05-20 15:29:10 +00:00
SourceValue : parameterValue ,
2022-05-19 18:04:44 +00:00
SourceScheme : codersdk . ParameterSourceSchemeData ,
DestinationScheme : parameterSchema . DefaultDestinationScheme ,
2022-02-12 19:34:04 +00:00
} )
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stdout )
2022-02-12 19:34:04 +00:00
}
2022-05-20 15:29:10 +00:00
// This recursion is only 1 level deep in practice.
// The first pass populates the missing parameters, so it does not enter this `if` block again.
2023-03-23 22:42:20 +00:00
return createValidTemplateVersion ( inv , args , parameters ... )
2022-02-12 19:34:04 +00:00
}
2022-03-22 19:17:50 +00:00
if version . Job . Status != codersdk . ProvisionerJobSucceeded {
return nil , nil , xerrors . New ( version . Job . Error )
2022-02-12 19:34:04 +00:00
}
2023-03-23 22:42:20 +00:00
resources , err := client . TemplateVersionResources ( inv . Context ( ) , version . ID )
2022-02-12 19:34:04 +00:00
if err != nil {
2022-03-22 19:17:50 +00:00
return nil , nil , err
2022-02-10 14:33:27 +00:00
}
2022-04-14 17:23:20 +00:00
2022-06-16 18:36:11 +00:00
// Only display the resources on the start transition, to avoid listing them more than once.
var startResources [ ] codersdk . WorkspaceResource
for _ , r := range resources {
if r . Transition == codersdk . WorkspaceTransitionStart {
startResources = append ( startResources , r )
}
}
2023-03-23 22:42:20 +00:00
err = cliui . WorkspaceResources ( inv . Stdout , startResources , cliui . WorkspaceResourcesOptions {
2022-04-11 23:54:30 +00:00
HideAgentState : true ,
HideAccess : true ,
Title : "Template Preview" ,
} )
2022-04-14 17:23:20 +00:00
if err != nil {
return nil , nil , xerrors . Errorf ( "preview template resources: %w" , err )
}
return & version , parameters , nil
2022-02-10 14:33:27 +00:00
}
2022-05-12 11:54:58 +00:00
// prettyDirectoryPath returns a prettified path when inside the users
// home directory. Falls back to dir if the users home directory cannot
// discerned. This function calls filepath.Clean on the result.
func prettyDirectoryPath ( dir string ) string {
dir = filepath . Clean ( dir )
homeDir , err := os . UserHomeDir ( )
if err != nil {
return dir
}
pretty := dir
if strings . HasPrefix ( pretty , homeDir ) {
pretty = strings . TrimPrefix ( pretty , homeDir )
pretty = "~" + pretty
}
return pretty
}
2022-11-16 22:34:06 +00:00
func ParseProvisionerTags ( rawTags [ ] string ) ( map [ string ] string , error ) {
tags := map [ string ] string { }
for _ , rawTag := range rawTags {
parts := strings . SplitN ( rawTag , "=" , 2 )
if len ( parts ) < 2 {
return nil , xerrors . Errorf ( "invalid tag format for %q. must be key=value" , rawTag )
}
tags [ parts [ 0 ] ] = parts [ 1 ]
}
return tags , nil
}