diff --git a/ci/integration/envs_test.go b/ci/integration/envs_test.go index cfa96983..634496f7 100644 --- a/ci/integration/envs_test.go +++ b/ci/integration/envs_test.go @@ -108,7 +108,7 @@ func TestEnvsCLI(t *testing.T) { // 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.StdoutMatches(regexp.QuoteMeta("Create a new Coder environment.")), tcli.StderrEmpty(), ) diff --git a/coder-sdk/client.go b/coder-sdk/client.go index 12a7c476..8076c517 100644 --- a/coder-sdk/client.go +++ b/coder-sdk/client.go @@ -6,7 +6,7 @@ import ( "net/url" ) -// Me is the route param to access resources of the authenticated user. +// Me is the user ID of the authenticated user. const Me = "me" // Client wraps the Coder HTTP API. diff --git a/coder-sdk/error.go b/coder-sdk/error.go index e5d16eb2..bdbef50f 100644 --- a/coder-sdk/error.go +++ b/coder-sdk/error.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "net/http/httputil" "golang.org/x/xerrors" ) @@ -34,16 +33,11 @@ type HTTPError struct { } func (e *HTTPError) Error() string { - dump, err := httputil.DumpResponse(e.Response, false) - if err != nil { - return fmt.Sprintf("dump response: %+v", err) - } - var msg APIError // Try to decode the payload as an error, if it fails or if there is no error message, // return the response URL with the dump. if err := json.NewDecoder(e.Response.Body).Decode(&msg); err != nil || msg.Err.Msg == "" { - return fmt.Sprintf("%s\n%s", e.Response.Request.URL, dump) + return fmt.Sprintf("%s: %d %s", e.Request.URL, e.StatusCode, e.Status) } // If the payload was a in the expected error format with a message, include it. @@ -51,5 +45,5 @@ func (e *HTTPError) Error() string { } func bodyError(resp *http.Response) error { - return &HTTPError{resp} + return &HTTPError{Response: resp} } diff --git a/docs/coder_envs.md b/docs/coder_envs.md index c89a5060..90d14880 100644 --- a/docs/coder_envs.md +++ b/docs/coder_envs.md @@ -22,6 +22,8 @@ Perform operations on the Coder environments owned by the active user. ### SEE ALSO * [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation +* [coder envs create](coder_envs_create.md) - create a new environment. +* [coder envs edit](coder_envs_edit.md) - edit an existing environment and initiate a rebuild. * [coder envs ls](coder_envs_ls.md) - list all environments owned by the active user * [coder envs rebuild](coder_envs_rebuild.md) - rebuild a Coder environment * [coder envs rm](coder_envs_rm.md) - remove Coder environments by name diff --git a/docs/coder_envs_create.md b/docs/coder_envs_create.md new file mode 100644 index 00000000..67c2cb8c --- /dev/null +++ b/docs/coder_envs_create.md @@ -0,0 +1,45 @@ +## coder envs create + +create a new environment. + +### Synopsis + +Create a new Coder environment. + +``` +coder envs create [environment_name] [flags] +``` + +### Examples + +``` +# create a new environment using default resource amounts +coder envs create my-new-env --image ubuntu +coder envs create my-new-powerful-env --cpu 12 --disk 100 --memory 16 --image ubuntu +``` + +### Options + +``` + -c, --cpu float32 number of cpu cores the environment should be provisioned with. + -d, --disk int GB of disk storage an environment should be provisioned with. + --follow follow buildlog after initiating rebuild + -g, --gpus int number GPUs an environment should be provisioned with. + -h, --help help for create + -i, --image string name of the image to base the environment off of. + -m, --memory float32 GB of RAM an environment should be provisioned with. + -o, --org string ID of the organization the environment should be created under. + -t, --tag string tag of the image the environment will be based off of. (default "latest") +``` + +### Options inherited from parent commands + +``` + --user string Specify the user whose resources to target (default "me") + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder envs](coder_envs.md) - Interact with Coder environments + diff --git a/docs/coder_envs_edit.md b/docs/coder_envs_edit.md new file mode 100644 index 00000000..72416b2c --- /dev/null +++ b/docs/coder_envs_edit.md @@ -0,0 +1,45 @@ +## coder envs edit + +edit an existing environment and initiate a rebuild. + +### Synopsis + +Edit an existing environment and initate a rebuild. + +``` +coder envs edit [flags] +``` + +### Examples + +``` +coder envs edit back-end-env --cpu 4 + +coder envs edit back-end-env --disk 20 +``` + +### Options + +``` + -c, --cpu float32 The number of cpu cores the environment should be provisioned with. + -d, --disk int The amount of disk storage an environment should be provisioned with. + --follow follow buildlog after initiating rebuild + -g, --gpu int The amount of disk storage to provision the environment with. + -h, --help help for edit + -i, --image string name of the image you want the environment to be based off of. + -m, --memory float32 The amount of RAM an environment should be provisioned with. + -o, --org string name of the organization the environment should be created under. + -t, --tag string image tag of the image you want to base the environment off of. (default "latest") +``` + +### Options inherited from parent commands + +``` + --user string Specify the user whose resources to target (default "me") + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder envs](coder_envs.md) - Interact with Coder environments + diff --git a/internal/cmd/ceapi.go b/internal/cmd/ceapi.go index 1e7e2480..8d155de1 100644 --- a/internal/cmd/ceapi.go +++ b/internal/cmd/ceapi.go @@ -84,13 +84,12 @@ func findEnv(ctx context.Context, client *coder.Client, envName, userEmail strin } type findImgConf struct { - client *coder.Client email string imgName string orgName string } -func findImg(ctx context.Context, conf findImgConf) (*coder.Image, error) { +func findImg(ctx context.Context, client *coder.Client, conf findImgConf) (*coder.Image, error) { switch { case conf.email == "": return nil, xerrors.New("user email unset") @@ -98,13 +97,10 @@ func findImg(ctx context.Context, conf findImgConf) (*coder.Image, error) { return nil, xerrors.New("image name unset") } - imgs, err := getImgs(ctx, - getImgsConf{ - client: conf.client, - email: conf.email, - orgName: conf.orgName, - }, - ) + imgs, err := getImgs(ctx, client, getImgsConf{ + email: conf.email, + orgName: conf.orgName, + }) if err != nil { return nil, err } @@ -129,30 +125,29 @@ func findImg(ctx context.Context, conf findImgConf) (*coder.Image, error) { return nil, xerrors.New("image not found - did you forget to import this image?") } - lines := []string{clog.Tipf("Did you mean?")} + lines := []string{clog.Hintf("Did you mean?")} for _, img := range possibleMatches { - lines = append(lines, img.Repository) + lines = append(lines, fmt.Sprintf(" %s", img.Repository)) } return nil, clog.Fatal( - fmt.Sprintf("Found %d possible matches for %q.", len(possibleMatches), conf.imgName), + fmt.Sprintf("image %s not found", conf.imgName), lines..., ) } type getImgsConf struct { - client *coder.Client email string orgName string } -func getImgs(ctx context.Context, conf getImgsConf) ([]coder.Image, error) { - u, err := conf.client.UserByEmail(ctx, conf.email) +func getImgs(ctx context.Context, client *coder.Client, conf getImgsConf) ([]coder.Image, error) { + u, err := client.UserByEmail(ctx, conf.email) if err != nil { return nil, err } - orgs, err := conf.client.Organizations(ctx) + orgs, err := client.Organizations(ctx) if err != nil { return nil, err } @@ -160,7 +155,7 @@ func getImgs(ctx context.Context, conf getImgsConf) ([]coder.Image, error) { orgs = lookupUserOrgs(u, orgs) for _, org := range orgs { - imgs, err := conf.client.OrganizationImages(ctx, org.ID) + imgs, err := client.OrganizationImages(ctx, org.ID) if err != nil { return nil, err } diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go index 4dff187b..a35fa6ed 100644 --- a/internal/cmd/envs.go +++ b/internal/cmd/envs.go @@ -141,6 +141,10 @@ coder envs --user charlie@coder.com ls -o json \ func createEnvCmd(user *string) *cobra.Command { var ( org string + cpu float32 + memory float32 + disk int + gpus int img string tag string follow bool @@ -150,14 +154,10 @@ func createEnvCmd(user *string) *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.", + Long: "Create a new Coder environment.", 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`, +coder envs create my-new-env --image ubuntu +coder envs create my-new-powerful-env --cpu 12 --disk 100 --memory 16 --image ubuntu`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() if img == "" { @@ -178,14 +178,11 @@ coder envs create --cpu 4 --disk 100 --memory 8 --image 5f443b16-30652892427b955 return xerrors.New("org is required for multi-org members") } - importedImg, err := findImg(ctx, - findImgConf{ - client: client, - email: *user, - imgName: img, - orgName: org, - }, - ) + importedImg, err := findImg(ctx, client, findImgConf{ + email: *user, + imgName: img, + orgName: org, + }) if err != nil { return err } @@ -195,13 +192,11 @@ coder envs create --cpu 4 --disk 100 --memory 8 --image 5f443b16-30652892427b955 Name: args[0], ImageID: importedImg.ID, ImageTag: tag, + CPUCores: cpu, + MemoryGB: memory, + DiskGB: disk, + GPUs: gpus, } - // We're explicitly ignoring errors for these because all we - // need to now is if the numeric type is 0 or not. - createReq.CPUCores, _ = cmd.Flags().GetFloat32("cpu") - createReq.MemoryGB, _ = cmd.Flags().GetFloat32("memory") - createReq.DiskGB, _ = cmd.Flags().GetInt("disk") - createReq.GPUs, _ = cmd.Flags().GetInt("gpus") // if any of these defaulted to their zero value we provision // the create request with the imported image defaults instead. @@ -230,17 +225,17 @@ coder envs create --cpu 4 --disk 100 --memory 8 --image 5f443b16-30652892427b955 clog.LogSuccess("creating environment...", clog.BlankLine, - clog.Tipf(`run "coder envs watch-build %q" to trail the build logs`, env.Name), + clog.Tipf(`run "coder envs watch-build %s" to trail the build logs`, env.Name), ) return nil }, } cmd.Flags().StringVarP(&org, "org", "o", "", "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", 0, "number of cpu cores the environment should be provisioned with.") - cmd.Flags().Float32P("memory", "m", 0, "GB of RAM an environment should be provisioned with.") - cmd.Flags().IntP("disk", "d", 0, "GB of disk storage an environment should be provisioned with.") - cmd.Flags().IntP("gpus", "g", 0, "number GPUs an environment should be provisioned with.") + cmd.Flags().Float32VarP(&cpu, "cpu", "c", 0, "number of cpu cores the environment should be provisioned with.") + cmd.Flags().Float32VarP(&memory, "memory", "m", 0, "GB of RAM an environment should be provisioned with.") + cmd.Flags().IntVarP(&disk, "disk", "d", 0, "GB of disk storage an environment should be provisioned with.") + cmd.Flags().IntVarP(&gpus, "gpus", "g", 0, "number GPUs an environment should be provisioned with.") cmd.Flags().StringVarP(&img, "image", "i", "", "name of the image to base the environment off of.") cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") _ = cmd.MarkFlagRequired("image") @@ -249,22 +244,21 @@ coder envs create --cpu 4 --disk 100 --memory 8 --image 5f443b16-30652892427b955 func editEnvCmd(user *string) *cobra.Command { var ( - org string - img string - tag string - cpuCores float32 - memGB float32 - diskGB int - gpus int - follow bool + org string + img string + tag string + cpu float32 + memory float32 + disk int + gpus int + follow bool ) cmd := &cobra.Command{ - Use: "edit", - Short: "edit an existing environment owned by the active user.", - Args: cobra.ExactArgs(1), - Hidden: true, - Long: "Edit an existing environment owned by the active user.", + Use: "edit", + Short: "edit an existing environment and initiate a rebuild.", + Args: cobra.ExactArgs(1), + Long: "Edit an existing environment and initate a rebuild.", Example: `coder envs edit back-end-env --cpu 4 coder envs edit back-end-env --disk 20`, @@ -292,25 +286,17 @@ coder envs edit back-end-env --disk 20`, return xerrors.New("org is required for multi-org members") } - cpuCores, _ = cmd.Flags().GetFloat32("cpu") - memGB, _ = cmd.Flags().GetFloat32("memory") - diskGB, _ = cmd.Flags().GetInt("disk") - gpus, _ = cmd.Flags().GetInt("gpus") - - req, err := buildUpdateReq(ctx, - updateConf{ - cpu: cpuCores, - memGB: memGB, - diskGB: diskGB, - gpus: gpus, - client: client, - environment: env, - user: user, - image: img, - imageTag: tag, - orgName: org, - }, - ) + req, err := buildUpdateReq(ctx, client, updateConf{ + cpu: cpu, + memGB: memory, + diskGB: disk, + gpus: gpus, + environment: env, + user: user, + image: img, + imageTag: tag, + orgName: org, + }) if err != nil { return err } @@ -329,7 +315,7 @@ coder envs edit back-end-env --disk 20`, clog.LogSuccess("applied changes to the environment, rebuilding...", clog.BlankLine, - clog.Tipf(`run "coder envs watch-build %q" to trail the build logs`, envName), + clog.Tipf(`run "coder envs watch-build %s" to trail the build logs`, envName), ) return nil }, @@ -337,10 +323,10 @@ coder envs edit back-end-env --disk 20`, cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the environment should be created under.") cmd.Flags().StringVarP(&img, "image", "i", "", "name of the image you want the environment to be based off of.") cmd.Flags().StringVarP(&tag, "tag", "t", "latest", "image tag of the image you want to base the environment off of.") - cmd.Flags().Float32P("cpu", "c", cpuCores, "The number of cpu cores the environment should be provisioned with.") - cmd.Flags().Float32P("memory", "m", memGB, "The amount of RAM an environment should be provisioned with.") - cmd.Flags().IntP("disk", "d", diskGB, "The amount of disk storage an environment should be provisioned with.") - cmd.Flags().IntP("gpu", "g", gpus, "The amount of disk storage to provision the environment with.") + cmd.Flags().Float32VarP(&cpu, "cpu", "c", 0, "The number of cpu cores the environment should be provisioned with.") + cmd.Flags().Float32VarP(&memory, "memory", "m", 0, "The amount of RAM an environment should be provisioned with.") + cmd.Flags().IntVarP(&disk, "disk", "d", 0, "The amount of disk storage an environment should be provisioned with.") + cmd.Flags().IntVarP(&gpus, "gpu", "g", 0, "The amount of disk storage to provision the environment with.") cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") return cmd } @@ -364,7 +350,7 @@ func rmEnvsCmd(user *string) *cobra.Command { } if _, err := confirm.Run(); err != nil { return clog.Fatal( - "failed to confirm prompt", clog.BlankLine, + "failed to confirm deletion", clog.BlankLine, clog.Tipf(`use "--force" to rebuild without a confirmation prompt`), ) } @@ -400,7 +386,6 @@ type updateConf struct { memGB float32 diskGB int gpus int - client *coder.Client environment *coder.Environment user *string image string @@ -408,7 +393,7 @@ type updateConf struct { orgName string } -func buildUpdateReq(ctx context.Context, conf updateConf) (*coder.UpdateEnvironmentReq, error) { +func buildUpdateReq(ctx context.Context, client *coder.Client, conf updateConf) (*coder.UpdateEnvironmentReq, error) { var ( updateReq coder.UpdateEnvironmentReq defaultCPUCores float32 @@ -418,14 +403,11 @@ func buildUpdateReq(ctx context.Context, conf updateConf) (*coder.UpdateEnvironm // If this is not empty it means the user is requesting to change the environment image. if conf.image != "" { - importedImg, err := findImg(ctx, - findImgConf{ - client: conf.client, - email: *conf.user, - imgName: conf.image, - orgName: conf.orgName, - }, - ) + importedImg, err := findImg(ctx, client, findImgConf{ + email: *conf.user, + imgName: conf.image, + orgName: conf.orgName, + }) if err != nil { return nil, err }