mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
30281852d6
* feat: Add bufferring to provisioner job logs This should improve overall build performance, and especially under load. It removes the old `id` column on the `provisioner_job_logs` table and replaces it with an auto-incrementing big integer to preserve order. Funny enough, we never had to care about order before because inserts would at minimum be 1ms different. Now they aren't, so the order needs to be preserved. * Fix log bufferring * Fix frontend log streaming * Fix JS test
340 lines
12 KiB
Go
340 lines
12 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/briandowns/spinner"
|
|
"github.com/google/uuid"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/cli/cliui"
|
|
"github.com/coder/coder/coderd/database"
|
|
"github.com/coder/coder/coderd/util/ptr"
|
|
"github.com/coder/coder/codersdk"
|
|
"github.com/coder/coder/provisionerd"
|
|
"github.com/coder/coder/provisionersdk"
|
|
)
|
|
|
|
func templateCreate() *cobra.Command {
|
|
var (
|
|
directory string
|
|
provisioner string
|
|
parameterFile string
|
|
maxTTL time.Duration
|
|
minAutostartInterval time.Duration
|
|
)
|
|
cmd := &cobra.Command{
|
|
Use: "create [name]",
|
|
Short: "Create a template from the current directory or as specified by flag",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
client, err := CreateClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
organization, err := CurrentOrganization(cmd, client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var templateName string
|
|
if len(args) == 0 {
|
|
templateName = filepath.Base(directory)
|
|
} else {
|
|
templateName = args[0]
|
|
}
|
|
|
|
if utf8.RuneCountInString(templateName) > 31 {
|
|
return xerrors.Errorf("Template name must be less than 32 characters")
|
|
}
|
|
|
|
_, err = client.TemplateByName(cmd.Context(), organization.ID, templateName)
|
|
if err == nil {
|
|
return xerrors.Errorf("A template already exists named %q!", templateName)
|
|
}
|
|
|
|
// Confirm upload of the directory.
|
|
prettyDir := prettyDirectoryPath(directory)
|
|
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
|
Text: fmt.Sprintf("Create and upload %q?", prettyDir),
|
|
IsConfirm: true,
|
|
Default: cliui.ConfirmYes,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
|
|
spin.Writer = cmd.OutOrStdout()
|
|
spin.Suffix = cliui.Styles.Keyword.Render(" Uploading directory...")
|
|
spin.Start()
|
|
defer spin.Stop()
|
|
archive, err := provisionersdk.Tar(directory, provisionersdk.TemplateArchiveLimit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, archive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
spin.Stop()
|
|
|
|
job, _, err := createValidTemplateVersion(cmd, createValidTemplateVersionArgs{
|
|
Client: client,
|
|
Organization: organization,
|
|
Provisioner: database.ProvisionerType(provisioner),
|
|
FileID: resp.ID,
|
|
ParameterFile: parameterFile,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
|
Text: "Confirm create?",
|
|
IsConfirm: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
createReq := codersdk.CreateTemplateRequest{
|
|
Name: templateName,
|
|
VersionID: job.ID,
|
|
MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()),
|
|
MinAutostartIntervalMillis: ptr.Ref(minAutostartInterval.Milliseconds()),
|
|
}
|
|
|
|
_, err = client.CreateTemplate(cmd.Context(), organization.ID, createReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "\n"+cliui.Styles.Wrap.Render(
|
|
"The "+cliui.Styles.Keyword.Render(templateName)+" template has been created at "+cliui.Styles.DateTimeStamp.Render(time.Now().Format(time.Stamp))+"! "+
|
|
"Developers can provision a workspace with this template using:")+"\n")
|
|
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Code.Render(fmt.Sprintf("coder create --template=%q [workspace name]", templateName)))
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
|
|
|
return nil
|
|
},
|
|
}
|
|
currentDirectory, _ := os.Getwd()
|
|
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")
|
|
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
|
|
cmd.Flags().StringVarP(¶meterFile, "parameter-file", "", "", "Specify a file path with parameter values.")
|
|
cmd.Flags().DurationVarP(&maxTTL, "max-ttl", "", 24*time.Hour, "Specify a maximum TTL for workspaces created from this template.")
|
|
cmd.Flags().DurationVarP(&minAutostartInterval, "min-autostart-interval", "", time.Hour, "Specify a minimum autostart interval for workspaces created from this template.")
|
|
// This is for testing!
|
|
err := cmd.Flags().MarkHidden("test.provisioner")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
cliui.AllowSkipPrompt(cmd)
|
|
return cmd
|
|
}
|
|
|
|
type createValidTemplateVersionArgs struct {
|
|
Name string
|
|
Client *codersdk.Client
|
|
Organization codersdk.Organization
|
|
Provisioner database.ProvisionerType
|
|
FileID uuid.UUID
|
|
ParameterFile string
|
|
// 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
|
|
}
|
|
|
|
func createValidTemplateVersion(cmd *cobra.Command, args createValidTemplateVersionArgs, parameters ...codersdk.CreateParameterRequest) (*codersdk.TemplateVersion, []codersdk.CreateParameterRequest, error) {
|
|
client := args.Client
|
|
|
|
req := codersdk.CreateTemplateVersionRequest{
|
|
Name: args.Name,
|
|
StorageMethod: codersdk.ProvisionerStorageMethodFile,
|
|
FileID: args.FileID,
|
|
Provisioner: codersdk.ProvisionerType(args.Provisioner),
|
|
ParameterValues: parameters,
|
|
}
|
|
if args.Template != nil {
|
|
req.TemplateID = args.Template.ID
|
|
}
|
|
version, err := client.CreateTemplateVersion(cmd.Context(), args.Organization.ID, req)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
err = cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
|
|
Fetch: func() (codersdk.ProvisionerJob, error) {
|
|
version, err := client.TemplateVersion(cmd.Context(), version.ID)
|
|
return version.Job, err
|
|
},
|
|
Cancel: func() error {
|
|
return client.CancelTemplateVersion(cmd.Context(), version.ID)
|
|
},
|
|
Logs: func() (<-chan codersdk.ProvisionerJobLog, io.Closer, error) {
|
|
return client.TemplateVersionLogsAfter(cmd.Context(), version.ID, 0)
|
|
},
|
|
})
|
|
if err != nil {
|
|
if !provisionerd.IsMissingParameterError(err.Error()) {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
version, err = client.TemplateVersion(cmd.Context(), version.ID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
parameterSchemas, err := client.TemplateVersionSchema(cmd.Context(), version.ID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
parameterValues, err := client.TemplateVersionParameters(cmd.Context(), version.ID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// 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 {
|
|
activeVersion, err := client.TemplateVersion(cmd.Context(), args.Template.ActiveVersionID)
|
|
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
|
|
values, err := client.Parameters(cmd.Context(), codersdk.ParameterImportJob, activeVersion.Job.ID)
|
|
if err != nil {
|
|
return nil, nil, xerrors.Errorf("Fetch previous version parameters: %w", err)
|
|
}
|
|
for _, value := range values {
|
|
lastParameterValues[value.Name] = value
|
|
}
|
|
}
|
|
|
|
if provisionerd.IsMissingParameterError(version.Job.Error) {
|
|
valuesBySchemaID := map[string]codersdk.ComputedParameter{}
|
|
for _, parameterValue := range parameterValues {
|
|
valuesBySchemaID[parameterValue.SchemaID.String()] = parameterValue
|
|
}
|
|
|
|
// parameterMapFromFile can be nil if parameter file is not specified
|
|
var parameterMapFromFile map[string]string
|
|
if args.ParameterFile != "" {
|
|
_, _ = 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, nil, err
|
|
}
|
|
}
|
|
|
|
// pulled params come from the last template version
|
|
pulled := make([]string, 0)
|
|
missingSchemas := make([]codersdk.ParameterSchema, 0)
|
|
for _, parameterSchema := range parameterSchemas {
|
|
_, ok := valuesBySchemaID[parameterSchema.ID.String()]
|
|
if ok {
|
|
continue
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
missingSchemas = append(missingSchemas, parameterSchema)
|
|
}
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("This template has required variables! They are scoped to the template, and not viewable after being set."))
|
|
if len(pulled) > 0 {
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render(fmt.Sprintf("The following parameter values are being pulled from the latest template version: %s.", strings.Join(pulled, ", "))))
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Use \"--always-prompt\" flag to change the values."))
|
|
}
|
|
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\r\n")
|
|
|
|
for _, parameterSchema := range missingSchemas {
|
|
parameterValue, err := getParameterValueFromMapOrInput(cmd, parameterMapFromFile, parameterSchema)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
parameters = append(parameters, codersdk.CreateParameterRequest{
|
|
Name: parameterSchema.Name,
|
|
SourceValue: parameterValue,
|
|
SourceScheme: codersdk.ParameterSourceSchemeData,
|
|
DestinationScheme: parameterSchema.DefaultDestinationScheme,
|
|
})
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
|
}
|
|
|
|
// 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.
|
|
return createValidTemplateVersion(cmd, args, parameters...)
|
|
}
|
|
|
|
if version.Job.Status != codersdk.ProvisionerJobSucceeded {
|
|
return nil, nil, xerrors.New(version.Job.Error)
|
|
}
|
|
|
|
resources, err := client.TemplateVersionResources(cmd.Context(), version.ID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
err = cliui.WorkspaceResources(cmd.OutOrStdout(), startResources, cliui.WorkspaceResourcesOptions{
|
|
HideAgentState: true,
|
|
HideAccess: true,
|
|
Title: "Template Preview",
|
|
})
|
|
if err != nil {
|
|
return nil, nil, xerrors.Errorf("preview template resources: %w", err)
|
|
}
|
|
|
|
return &version, parameters, nil
|
|
}
|
|
|
|
// 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
|
|
}
|