diff --git a/ci/integration/envs_test.go b/ci/integration/envs_test.go new file mode 100644 index 00000000..2f3c19c8 --- /dev/null +++ b/ci/integration/envs_test.go @@ -0,0 +1,69 @@ +package integration + +import ( + "context" + "regexp" + "testing" + + "cdr.dev/coder-cli/ci/tcli" +) + +// From Coder organization images +const ubuntuImgID = "5f443b16-30652892427b955601330fa5" + +func TestEnvsCLI(t *testing.T) { + t.Parallel() + + run(t, "coder-cli-env-tests", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { + headlessLogin(ctx, t, c) + + // Ensure binary is present. + c.Run(ctx, "which coder").Assert(t, + tcli.Success(), + tcli.StdoutMatches("/usr/sbin/coder"), + tcli.StderrEmpty(), + ) + + // Minimum args not received. + c.Run(ctx, "coder envs create").Assert(t, + tcli.StderrMatches(regexp.QuoteMeta("accepts 1 arg(s), received 0")), + tcli.Error(), + ) + + // Successfully output help. + c.Run(ctx, "coder envs create --help").Assert(t, + tcli.Success(), + tcli.StdoutMatches(regexp.QuoteMeta("Create a new environment under the active user.")), + tcli.StderrEmpty(), + ) + + // TODO(Faris) : uncomment this when we can safely purge the environments + // the integrations tests would create in the sidecar + // Successfully create environment. + // c.Run(ctx, "coder envs create --image "+ubuntuImgID+" test-ubuntu").Assert(t, + // tcli.Success(), + // // why does flog.Success write to stderr? + // tcli.StderrMatches(regexp.QuoteMeta("Successfully created environment \"test-ubuntu\"")), + // ) + + // Invalid environment name should fail. + c.Run(ctx, "coder envs create --image "+ubuntuImgID+" this-IS-an-invalid-EnvironmentName").Assert(t, + tcli.Error(), + tcli.StderrMatches(regexp.QuoteMeta("environment name must conform to regex ^[a-z0-9]([a-z0-9-]+)?")), + ) + + // TODO(Faris) : uncomment this when we can safely purge the environments + // the integrations tests would create in the sidecar + // Successfully provision environment with fractional resource amounts + // c.Run(ctx, fmt.Sprintf(`coder envs create -i %s -c 1.2 -m 1.4 non-whole-resource-amounts`, ubuntuImgID)).Assert(t, + // tcli.Success(), + // tcli.StderrMatches(regexp.QuoteMeta("Successfully created environment \"non-whole-resource-amounts\"")), + // ) + + // Image does not exist should fail. + c.Run(ctx, "coder envs create --image does-not-exist env-will-not-be-created").Assert(t, + tcli.Error(), + tcli.StderrMatches(regexp.QuoteMeta("does not exist")), + ) + }) +} diff --git a/ci/steps/gendocs.sh b/ci/steps/gendocs.sh index 9e31b626..cc397e4f 100755 --- a/ci/steps/gendocs.sh +++ b/ci/steps/gendocs.sh @@ -11,12 +11,6 @@ rm -rf ./docs mkdir ./docs go run ./cmd/coder gen-docs ./docs -# remove cobra footer from each file -for filename in ./docs/*.md; do - trimmed=$(head -n -1 "$filename") - echo "$trimmed" >$filename -done - if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then echo "Documentation needs generation:" git -c color.ui=always status | grep --color=no '\e\[31m' diff --git a/coder-sdk/env.go b/coder-sdk/env.go index 6bc2f2ba..2eced5f1 100644 --- a/coder-sdk/env.go +++ b/coder-sdk/env.go @@ -78,7 +78,7 @@ type CreateEnvironmentRequest struct { ImageID string `json:"image_id"` ImageTag string `json:"image_tag"` CPUCores float32 `json:"cpu_cores"` - MemoryGB int `json:"memory_gb"` + MemoryGB float32 `json:"memory_gb"` DiskGB int `json:"disk_gb"` GPUs int `json:"gpus"` Services []string `json:"services"` diff --git a/docs/coder.md b/docs/coder.md index 844267d5..9542c51d 100644 --- a/docs/coder.md +++ b/docs/coder.md @@ -25,3 +25,4 @@ coder provides a CLI for working with an existing Coder Enterprise installation * [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder environment * [coder urls](coder_urls.md) - Interact with environment DevURLs * [coder users](coder_users.md) - Interact with Coder user accounts + diff --git a/docs/coder_completion.md b/docs/coder_completion.md index 44f5519a..505fb8ca 100644 --- a/docs/coder_completion.md +++ b/docs/coder_completion.md @@ -67,3 +67,4 @@ MacOS: ### SEE ALSO * [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation + diff --git a/docs/coder_config-ssh.md b/docs/coder_config-ssh.md index b9f1e882..8e076fca 100644 --- a/docs/coder_config-ssh.md +++ b/docs/coder_config-ssh.md @@ -27,3 +27,4 @@ coder config-ssh [flags] ### SEE ALSO * [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation + diff --git a/docs/coder_envs.md b/docs/coder_envs.md index af394d16..176ab6b8 100644 --- a/docs/coder_envs.md +++ b/docs/coder_envs.md @@ -24,3 +24,4 @@ Perform operations on the Coder environments owned by the active user. * [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation * [coder envs ls](coder_envs_ls.md) - list all environments owned by the active user * [coder envs stop](coder_envs_stop.md) - stop Coder environments by name + diff --git a/docs/coder_envs_ls.md b/docs/coder_envs_ls.md index 49cd04dd..d4b78438 100644 --- a/docs/coder_envs_ls.md +++ b/docs/coder_envs_ls.md @@ -27,3 +27,4 @@ coder envs ls [flags] ### SEE ALSO * [coder envs](coder_envs.md) - Interact with Coder environments + diff --git a/docs/coder_envs_stop.md b/docs/coder_envs_stop.md index 1348a14c..b6cc6ec6 100644 --- a/docs/coder_envs_stop.md +++ b/docs/coder_envs_stop.md @@ -41,3 +41,4 @@ coder envs --user charlie@coder.com ls -o json \ ### SEE ALSO * [coder envs](coder_envs.md) - Interact with Coder environments + diff --git a/docs/coder_login.md b/docs/coder_login.md index bd3d9fb6..d4518229 100644 --- a/docs/coder_login.md +++ b/docs/coder_login.md @@ -25,3 +25,4 @@ coder login [Coder Enterprise URL eg. https://my.coder.domain/] [flags] ### SEE ALSO * [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation + diff --git a/docs/coder_logout.md b/docs/coder_logout.md index a41aa009..d7cfcef7 100644 --- a/docs/coder_logout.md +++ b/docs/coder_logout.md @@ -25,3 +25,4 @@ coder logout [flags] ### SEE ALSO * [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation + diff --git a/docs/coder_secrets.md b/docs/coder_secrets.md index b8178fd5..dec20226 100644 --- a/docs/coder_secrets.md +++ b/docs/coder_secrets.md @@ -26,3 +26,4 @@ Interact with secrets objects owned by the active user. * [coder secrets ls](coder_secrets_ls.md) - List all secrets owned by the active user * [coder secrets rm](coder_secrets_rm.md) - Remove one or more secrets by name * [coder secrets view](coder_secrets_view.md) - View a secret by name + diff --git a/docs/coder_secrets_create.md b/docs/coder_secrets_create.md index 3dfa1140..17255174 100644 --- a/docs/coder_secrets_create.md +++ b/docs/coder_secrets_create.md @@ -38,3 +38,4 @@ coder secrets create aws-credentials --from-file ./credentials.json ### SEE ALSO * [coder secrets](coder_secrets.md) - Interact with Coder Secrets + diff --git a/docs/coder_secrets_ls.md b/docs/coder_secrets_ls.md index e3e0ebc0..cbcd3dd4 100644 --- a/docs/coder_secrets_ls.md +++ b/docs/coder_secrets_ls.md @@ -26,3 +26,4 @@ coder secrets ls [flags] ### SEE ALSO * [coder secrets](coder_secrets.md) - Interact with Coder Secrets + diff --git a/docs/coder_secrets_rm.md b/docs/coder_secrets_rm.md index c8877d82..6c91b2bf 100644 --- a/docs/coder_secrets_rm.md +++ b/docs/coder_secrets_rm.md @@ -32,3 +32,4 @@ coder secrets rm mysql-password mysql-user ### SEE ALSO * [coder secrets](coder_secrets.md) - Interact with Coder Secrets + diff --git a/docs/coder_secrets_view.md b/docs/coder_secrets_view.md index 60fcaa4d..1a468017 100644 --- a/docs/coder_secrets_view.md +++ b/docs/coder_secrets_view.md @@ -32,3 +32,4 @@ coder secrets view mysql-password ### SEE ALSO * [coder secrets](coder_secrets.md) - Interact with Coder Secrets + diff --git a/docs/coder_sh.md b/docs/coder_sh.md index 8bc0d959..8bd80ac6 100644 --- a/docs/coder_sh.md +++ b/docs/coder_sh.md @@ -31,3 +31,4 @@ coder sh backend-env ### SEE ALSO * [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation + diff --git a/docs/coder_sync.md b/docs/coder_sync.md index 91098662..47949594 100644 --- a/docs/coder_sync.md +++ b/docs/coder_sync.md @@ -26,3 +26,4 @@ coder sync [local directory] [:] [flags] ### SEE ALSO * [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation + diff --git a/docs/coder_urls.md b/docs/coder_urls.md index 75f361f4..c725787b 100644 --- a/docs/coder_urls.md +++ b/docs/coder_urls.md @@ -24,3 +24,4 @@ Interact with environment DevURLs * [coder urls create](coder_urls_create.md) - Create a new devurl for an environment * [coder urls ls](coder_urls_ls.md) - List all DevURLs for an environment * [coder urls rm](coder_urls_rm.md) - Remove a dev url + diff --git a/docs/coder_urls_create.md b/docs/coder_urls_create.md index 5d613f56..5df9b96f 100644 --- a/docs/coder_urls_create.md +++ b/docs/coder_urls_create.md @@ -27,3 +27,4 @@ coder urls create [env_name] [port] [--access ] [--name ] [flags] ### SEE ALSO * [coder urls](coder_urls.md) - Interact with environment DevURLs + diff --git a/docs/coder_urls_ls.md b/docs/coder_urls_ls.md index 876210e9..bd01eb56 100644 --- a/docs/coder_urls_ls.md +++ b/docs/coder_urls_ls.md @@ -26,3 +26,4 @@ coder urls ls [environment_name] [flags] ### SEE ALSO * [coder urls](coder_urls.md) - Interact with environment DevURLs + diff --git a/docs/coder_urls_rm.md b/docs/coder_urls_rm.md index e19ccf30..ff961448 100644 --- a/docs/coder_urls_rm.md +++ b/docs/coder_urls_rm.md @@ -25,3 +25,4 @@ coder urls rm [environment_name] [port] [flags] ### SEE ALSO * [coder urls](coder_urls.md) - Interact with environment DevURLs + diff --git a/docs/coder_users.md b/docs/coder_users.md index 59a8c779..332d1824 100644 --- a/docs/coder_users.md +++ b/docs/coder_users.md @@ -22,3 +22,4 @@ Interact with Coder user accounts * [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation * [coder users ls](coder_users_ls.md) - list all user accounts + diff --git a/docs/coder_users_ls.md b/docs/coder_users_ls.md index 99b00db2..e9624a83 100644 --- a/docs/coder_users_ls.md +++ b/docs/coder_users_ls.md @@ -33,3 +33,4 @@ coder users ls -o json | jq .[] | jq -r .email ### SEE ALSO * [coder users](coder_users.md) - Interact with Coder user accounts + diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 8917b22e..d00796cd 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -13,10 +13,11 @@ var verbose bool = false // Make constructs the "coder" root command func Make() *cobra.Command { app := &cobra.Command{ - Use: "coder", - Short: "coder provides a CLI for working with an existing Coder Enterprise installation", - SilenceErrors: true, - SilenceUsage: true, + Use: "coder", + Short: "coder provides a CLI for working with an existing Coder Enterprise installation", + SilenceErrors: true, + SilenceUsage: true, + DisableAutoGenTag: true, } app.AddCommand( diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go index 50020eba..63d8f885 100644 --- a/internal/cmd/envs.go +++ b/internal/cmd/envs.go @@ -14,6 +14,15 @@ import ( "golang.org/x/xerrors" ) +const ( + defaultOrg = "default" + defaultImgTag = "latest" + defaultCPUCores float32 = 1 + defaultMemGB float32 = 1 + defaultDiskGB = 10 + defaultGPUs = 0 +) + func envsCommand() *cobra.Command { var outputFmt string var user string @@ -64,9 +73,9 @@ func envsCommand() *cobra.Command { lsCmd.Flags().StringVarP(&outputFmt, "output", "o", "human", "human | json") cmd.AddCommand(lsCmd) cmd.AddCommand(stopEnvCommand(&user)) - cmd.AddCommand(watchBuildLogCommand()) cmd.AddCommand(rebuildEnvCommand()) + cmd.AddCommand(createEnvCommand()) return cmd } @@ -125,3 +134,76 @@ coder envs --user charlie@coder.com ls -o json \ }, } } + +func createEnvCommand() *cobra.Command { + var ( + org string + img string + tag string + follow bool + ) + + cmd := &cobra.Command{ + Use: "create [environment_name]", + Short: "create a new environment.", + Args: cobra.ExactArgs(1), + // Don't unhide this command until we can pass image names instead of image id's. + Hidden: true, + Long: "Create a new environment under the active user.", + Example: `# create a new environment using default resource amounts +coder envs create --image 5f443b16-30652892427b955601330fa5 my-env-name + +# create a new environment using custom resource amounts +coder envs create --cpu 4 --disk 100 --memory 8 --image 5f443b16-30652892427b955601330fa5 my-env-name`, + RunE: func(cmd *cobra.Command, args []string) error { + if img == "" { + return xerrors.New("image id unset") + } + // ExactArgs(1) ensures our name value can't panic on an out of bounds. + createReq := &coder.CreateEnvironmentRequest{ + Name: args[0], + ImageID: img, + ImageTag: tag, + } + // We're explicitly ignoring errors for these because all of these flags + // have a non-zero-value default value set already. + createReq.CPUCores, _ = cmd.Flags().GetFloat32("cpu") + createReq.MemoryGB, _ = cmd.Flags().GetFloat32("memory") + createReq.DiskGB, _ = cmd.Flags().GetInt("disk") + createReq.GPUs, _ = cmd.Flags().GetInt("gpus") + + client, err := newClient() + if err != nil { + return err + } + + env, err := client.CreateEnvironment(cmd.Context(), org, *createReq) + if err != nil { + return xerrors.Errorf("create environment: %w", err) + } + + clog.LogSuccess( + "creating environment...", + clog.BlankLine, + clog.Tip(`run "coder envs watch-build %q" to trail the build logs`, args[0]), + ) + + if follow { + if err := trailBuildLogs(cmd.Context(), client, env.ID); err != nil { + return err + } + } + return nil + }, + } + cmd.Flags().StringVarP(&org, "org", "o", defaultOrg, "ID of the organization the environment should be created under.") + cmd.Flags().StringVarP(&tag, "tag", "t", defaultImgTag, "tag of the image the environment will be based off of.") + cmd.Flags().Float32P("cpu", "c", defaultCPUCores, "number of cpu cores the environment should be provisioned with.") + cmd.Flags().Float32P("memory", "m", defaultMemGB, "GB of RAM an environment should be provisioned with.") + cmd.Flags().IntP("disk", "d", defaultDiskGB, "GB of disk storage an environment should be provisioned with.") + cmd.Flags().IntP("gpus", "g", defaultGPUs, "number GPUs an environment should be provisioned with.") + cmd.Flags().StringVarP(&img, "image", "i", "", "ID of the image to base the environment off of.") + cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") + cmd.MarkFlagRequired("image") + return cmd +}