Skip to content

feat: Add "coder projects create" command #246

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Feb 12, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Create project fully works!!!
  • Loading branch information
kylecarbs committed Feb 10, 2022
commit dc86c0e8cf1d221a2f5c81a4b0701ab7a43d25fa
273 changes: 152 additions & 121 deletions cli/projectcreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ import (
"io"
"os"
"path/filepath"
"strings"
"time"

"github.com/briandowns/spinner"
"github.com/fatih/color"
"github.com/google/uuid"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"github.com/xlab/treeprint"
"golang.org/x/xerrors"

"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/parameter"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/database"
"github.com/coder/coder/provisionerd"
Expand Down Expand Up @@ -62,150 +65,178 @@ func projectCreate() *cobra.Command {
return err
}

spin := spinner.New(spinner.CharSets[0], 25*time.Millisecond)
spin.Suffix = " Uploading current directory..."
spin.Start()

defer spin.Stop()

bytes, err := tarDirectory(directory)
job, err := doProjectLoop(cmd, client, organization, directory, []coderd.CreateParameterValueRequest{})
if err != nil {
return err
}

resp, err := client.UploadFile(cmd.Context(), codersdk.ContentTypeTar, bytes)
project, err := client.CreateProject(cmd.Context(), organization.Name, coderd.CreateProjectRequest{
Name: name,
VersionImportJobID: job.ID,
})
if err != nil {
return err
}

job, err := client.CreateProjectVersionImportProvisionerJob(cmd.Context(), organization.Name, coderd.CreateProjectImportJobRequest{
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: resp.Hash,
Provisioner: database.ProvisionerTypeTerraform,
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s The %s project has been created!\n", color.HiBlackString(">"), color.HiCyanString(project.Name))
_, err = runPrompt(cmd, &promptui.Prompt{
Label: "Create a new workspace?",
IsConfirm: true,
Default: "y",
})
if err != nil {
return err
}
spin.Stop()

logs, err := client.FollowProvisionerJobLogsAfter(cmd.Context(), organization.Name, job.ID, time.Time{})
if err != nil {
return err
}
for {
log, ok := <-logs
if !ok {
break
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", color.HiGreenString("[tf]"), log.Output)
}
fmt.Printf("Create a new workspace now!\n")
return nil
},
}
currentDirectory, _ := os.Getwd()
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")

job, err = client.ProvisionerJob(cmd.Context(), organization.Name, job.ID)
if err != nil {
return err
}
return cmd
}

if provisionerd.IsMissingParameterError(job.Error) {
fmt.Printf("Missing something!\n")
return nil
}
func doProjectLoop(cmd *cobra.Command, client *codersdk.Client, organization coderd.Organization, directory string, params []coderd.CreateParameterValueRequest) (*coderd.ProvisionerJob, error) {
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
spin.Writer = cmd.OutOrStdout()
spin.Suffix = " Uploading current directory..."
spin.Color("fgHiGreen")
spin.Start()
defer spin.Stop()

resources, err := client.ProvisionerJobResources(cmd.Context(), organization.Name, job.ID)
if err != nil {
return err
}
bytes, err := tarDirectory(directory)
if err != nil {
return nil, err
}

resp, err := client.UploadFile(cmd.Context(), codersdk.ContentTypeTar, bytes)
if err != nil {
return nil, err
}

fmt.Printf("Resources: %+v\n", resources)
job, err := client.CreateProjectVersionImportProvisionerJob(cmd.Context(), organization.Name, coderd.CreateProjectImportJobRequest{
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: resp.Hash,
Provisioner: database.ProvisionerTypeTerraform,
ParameterValues: params,
})
if err != nil {
return nil, err
}

project, err := client.CreateProject(cmd.Context(), organization.Name, coderd.CreateProjectRequest{
Name: name,
VersionImportJobID: job.ID,
spin.Suffix = " Waiting for the import to complete..."

logs, err := client.FollowProvisionerJobLogsAfter(cmd.Context(), organization.Name, job.ID, time.Time{})
if err != nil {
return nil, err
}
for {
_, ok := <-logs
if !ok {
break
}
// _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", color.HiGreenString("[tf]"), log.Output)
}

job, err = client.ProvisionerJob(cmd.Context(), organization.Name, job.ID)
if err != nil {
return nil, err
}

parameterSchemas, err := client.ProvisionerJobParameterSchemas(cmd.Context(), organization.Name, job.ID)
if err != nil {
return nil, err
}
parameterValues, err := client.ProvisionerJobParameterValues(cmd.Context(), organization.Name, job.ID)
if err != nil {
return nil, err
}
spin.Stop()

if provisionerd.IsMissingParameterError(job.Error) {
valuesBySchemaID := map[string]coderd.ComputedParameterValue{}
for _, parameterValue := range parameterValues {
valuesBySchemaID[parameterValue.SchemaID.String()] = parameterValue
}
for _, parameterSchema := range parameterSchemas {
_, ok := valuesBySchemaID[parameterSchema.ID.String()]
if ok {
continue
}
if parameterSchema.Name == parameter.CoderWorkspaceTransition {
continue
}
value, err := runPrompt(cmd, &promptui.Prompt{
Label: fmt.Sprintf("Enter value for %s:", color.HiCyanString(parameterSchema.Name)),
})
if err != nil {
return err
return nil, err
}
params = append(params, coderd.CreateParameterValueRequest{
Name: parameterSchema.Name,
SourceValue: value,
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: parameterSchema.DefaultDestinationScheme,
})
}
return doProjectLoop(cmd, client, organization, directory, params)
}

fmt.Printf("Project: %+v\n", project)

// _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Parsed project source... displaying parameters:")

// schemas, err := client.ProvisionerJobParameterSchemas(cmd.Context(), organization.Name, job.ID)
// if err != nil {
// return err
// }

// values, err := client.ProvisionerJobParameterValues(cmd.Context(), organization.Name, job.ID)
// if err != nil {
// return err
// }
// valueBySchemaID := map[string]coderd.ComputedParameterValue{}
// for _, value := range values {
// valueBySchemaID[value.SchemaID.String()] = value
// }

// for _, schema := range schemas {
// if value, ok := valueBySchemaID[schema.ID.String()]; ok {
// fmt.Printf("Value for: %s %s\n", value.Name, value.SourceValue)
// continue
// }
// fmt.Printf("No value for: %s\n", schema.Name)
// }

// schemas, err := client.ProvisionerJobParameterSchemas(cmd.Context(), organization.Name, job.ID)
// if err != nil {
// return err
// }
// _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n %s\n\n", color.HiBlackString("Parameters"))

// for _, param := range params {
// if param.Value == nil {
// _, _ = fmt.Fprintf(cmd.OutOrStdout(), " %s = must be set\n", color.HiRedString(param.Schema.Name))
// continue
// }
// value := param.Value.DestinationValue
// if !param.Schema.RedisplayValue {
// value = "<redacted>"
// }
// output := fmt.Sprintf(" %s = %s", color.HiGreenString(param.Value.SourceValue), color.CyanString(value))
// param.Value.DefaultSourceValue = false
// param.Value.Scope = database.ParameterScopeOrganization
// param.Value.ScopeID = organization.ID
// if param.Value.DefaultSourceValue {
// output += " (default value)"
// } else {
// output += fmt.Sprintf(" (inherited from %s)", param.Value.Scope)
// }
// root := treeprint.NewWithRoot(output)
// root.AddNode(color.HiBlackString("Description") + "\n" + param.Schema.Description)
// fmt.Fprintln(cmd.OutOrStdout(), strings.Join(strings.Split(root.String(), "\n"), "\n "))
// }

// for _, param := range params {
// if param.Value != nil {
// continue
// }

// value, err := runPrompt(cmd, &promptui.Prompt{
// Label: "Specify value for " + color.HiCyanString(param.Schema.Name),
// Validate: func(s string) error {
// // param.Schema.Vali
// return nil
// },
// })
// if err != nil {
// continue
// }
// fmt.Printf(": %s\n", value)
// }

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Create project %q!\n", name)
return nil
},
if job.Status != coderd.ProvisionerJobStatusSucceeded {
return nil, xerrors.New(job.Error)
}
currentDirectory, _ := os.Getwd()
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")

return cmd
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Successfully imported project source!\n", color.HiGreenString("✓"))

resources, err := client.ProvisionerJobResources(cmd.Context(), organization.Name, job.ID)
if err != nil {
return nil, err
}
return &job, outputProjectInformation(cmd, parameterSchemas, parameterValues, resources)
}

func outputProjectInformation(cmd *cobra.Command, parameterSchemas []coderd.ParameterSchema, parameterValues []coderd.ComputedParameterValue, resources []coderd.ProjectImportJobResource) error {
schemaByID := map[string]coderd.ParameterSchema{}
for _, schema := range parameterSchemas {
schemaByID[schema.ID.String()] = schema
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n %s\n\n", color.HiBlackString("Parameters"))
for _, value := range parameterValues {
schema, ok := schemaByID[value.SchemaID.String()]
if !ok {
return xerrors.Errorf("schema not found: %s", value.Name)
}
displayValue := value.SourceValue
if !schema.RedisplayValue {
displayValue = "<redacted>"
}
output := fmt.Sprintf("%s %s %s", color.HiCyanString(value.Name), color.HiBlackString("="), displayValue)
if value.DefaultSourceValue {
output += " (default value)"
} else if value.Scope != database.ParameterScopeImportJob {
output += fmt.Sprintf(" (inherited from %s)", value.Scope)
}

root := treeprint.NewWithRoot(output)
if schema.Description != "" {
root.AddBranch(fmt.Sprintf("%s\n%s\n", color.HiBlackString("Description"), schema.Description))
}
if schema.AllowOverrideSource {
root.AddBranch(fmt.Sprintf("%s Users can customize this value!", color.HiYellowString("+")))
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.Join(strings.Split(root.String(), "\n"), "\n "))
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " %s\n\n", color.HiBlackString("Resources"))
for _, resource := range resources {
transition := color.HiGreenString("start")
if resource.Transition == database.WorkspaceTransitionStop {
transition = color.HiRedString("stop")
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " %s %s on %s\n\n", color.HiCyanString(resource.Type), color.HiCyanString(resource.Name), transition)
}
return nil
}

func tarDirectory(directory string) ([]byte, error) {
Expand Down
6 changes: 3 additions & 3 deletions provisioner/terraform/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ func convertVariableToParameter(variable *tfconfig.Variable) (*proto.ParameterSc
Description: variable.Description,
RedisplayValue: !variable.Sensitive,
ValidationValueType: variable.Type,
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
}

if variable.Default != nil {
Expand All @@ -57,9 +60,6 @@ func convertVariableToParameter(variable *tfconfig.Variable) (*proto.ParameterSc
Scheme: proto.ParameterSource_DATA,
Value: string(defaultData),
}
schema.DefaultDestination = &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
}
}

if len(variable.Validations) > 0 && variable.Validations[0].Condition != nil {
Expand Down
9 changes: 7 additions & 2 deletions provisioner/terraform/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ func TestParse(t *testing.T) {
Name: "A",
RedisplayValue: true,
Description: "Testing!",
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
}},
},
},
Expand Down Expand Up @@ -100,8 +103,10 @@ func TestParse(t *testing.T) {
RedisplayValue: true,
ValidationCondition: `var.A == "value"`,
ValidationTypeSystem: proto.ParameterSchema_HCL,
},
},
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
}},
},
},
},
Expand Down
10 changes: 0 additions & 10 deletions provisionerd/proto/provisionerd.proto
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,6 @@ message CancelledJob {
string error = 2;
}

// TransitionedResource represents a resource that knows whether
// it's existence is dependent on stop or not.
//
// This is used on import to display start + stopped resources
// for the lifecycle of a workspace.
message TransitionedResource {
provisioner.Resource resource = 1;
bool destroy_on_stop = 2;
}

// CompletedJob is sent when the provisioner daemon completes a job.
message CompletedJob {
message WorkspaceProvision {
Expand Down