Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Change coder create | edit envs image flag to take image name and source defaults from image #159

Merged
merged 1 commit into from
Oct 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
26 changes: 13 additions & 13 deletions ci/integration/envs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

// From Coder organization images
const ubuntuImgID = "5f443b16-30652892427b955601330fa5"
// const ubuntuImgID = "5f443b16-30652892427b955601330fa5"

func TestEnvsCLI(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -37,6 +37,18 @@ func TestEnvsCLI(t *testing.T) {
tcli.StderrEmpty(),
)

// Image unset.
c.Run(ctx, "coder envs create test-env").Assert(t,
tcli.StderrMatches(regexp.QuoteMeta("fatal: required flag(s) \"image\" not set")),
tcli.Error(),
)

// Image not imported.
c.Run(ctx, "coder envs create test-env --image doesntmatter").Assert(t,
tcli.StderrMatches(regexp.QuoteMeta("fatal: image not found - did you forget to import this image?")),
tcli.Error(),
)

// TODO(Faris) : uncomment this when we can safely purge the environments
// the integrations tests would create in the sidecar
// Successfully create environment.
Expand All @@ -46,24 +58,12 @@ func TestEnvsCLI(t *testing.T) {
// 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")),
)
})
}
11 changes: 10 additions & 1 deletion coder-sdk/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Image struct {
Description string `json:"description"`
URL string `json:"url"` // User-supplied URL for image.
DefaultCPUCores float32 `json:"default_cpu_cores"`
DefaultMemoryGB int `json:"default_memory_gb"`
DefaultMemoryGB float32 `json:"default_memory_gb"`
DefaultDiskGB int `json:"default_disk_gb"`
Deprecated bool `json:"deprecated"`
}
Expand Down Expand Up @@ -47,3 +47,12 @@ func (c Client) ImportImage(ctx context.Context, orgID string, req ImportImageRe
}
return &img, nil
}

// OrganizationImages returns all of the images imported for orgID.
func (c Client) OrganizationImages(ctx context.Context, orgID string) ([]Image, error) {
var imgs []Image
if err := c.requestBody(ctx, http.MethodGet, "/api/orgs/"+orgID+"/images", nil, &imgs); err != nil {
return nil, err
}
return imgs, nil
}
10 changes: 10 additions & 0 deletions internal/clog/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ func LogSuccess(header string, lines ...string) {
}.String())
}

// LogWarn prints the given warn message to stderr.
func LogWarn(header string, lines ...string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

fmt.Fprint(os.Stderr, CLIMessage{
Level: "warning",
Color: color.FgYellow,
Header: header,
Lines: lines,
}.String())
}

// Warn creates an error with the level "warning".
func Warn(header string, lines ...string) CLIError {
return CLIError{
Expand Down
109 changes: 109 additions & 0 deletions internal/cmd/ceapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"strings"

"cdr.dev/coder-cli/coder-sdk"
"cdr.dev/coder-cli/internal/clog"
Expand Down Expand Up @@ -81,3 +82,111 @@ func findEnv(ctx context.Context, client *coder.Client, envName, userEmail strin
clog.Tipf("run \"coder envs ls\" to view your environments"),
)
}

type findImgConf struct {
client *coder.Client
email string
imgName string
orgName string
}

func findImg(ctx context.Context, conf findImgConf) (*coder.Image, error) {
switch {
case conf.email == "":
return nil, xerrors.New("user email unset")
case conf.imgName == "":
return nil, xerrors.New("image name unset")
}

imgs, err := getImgs(ctx,
getImgsConf{
client: conf.client,
email: conf.email,
orgName: conf.orgName,
},
)
if err != nil {
return nil, err
}

var possibleMatches []coder.Image

// The user may provide an image thats not an exact match
// to one of their imported images but they may be close.
// We can assist the user by collecting images that contain
// the user provided image flag value as a substring.
for _, img := range imgs {
// If it's an exact match we can just return and exit.
if img.Repository == conf.imgName {
return &img, nil
}
if strings.Contains(img.Repository, conf.imgName) {
possibleMatches = append(possibleMatches, img)
}
}

if len(possibleMatches) == 0 {
return nil, xerrors.New("image not found - did you forget to import this image?")
}

lines := []string{clog.Tipf("Did you mean?")}

for _, img := range possibleMatches {
lines = append(lines, img.Repository)
}
return nil, clog.Fatal(
fmt.Sprintf("Found %d possible matches for %q.", len(possibleMatches), 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)
if err != nil {
return nil, err
}

orgs, err := conf.client.Organizations(ctx)
if err != nil {
return nil, err
}

orgs = lookupUserOrgs(u, orgs)

for _, org := range orgs {
imgs, err := conf.client.OrganizationImages(ctx, org.ID)
if err != nil {
return nil, err
}
// If orgName is set we know the user is a multi-org member
// so we should only return the imported images that beong to the org they specified.
if conf.orgName != "" && conf.orgName == org.Name {
return imgs, nil
}

if conf.orgName == "" {
// if orgName is unset we know the user is only part of one org.
return imgs, nil
}
}
return nil, xerrors.Errorf("org name %q not found", conf.orgName)
}

func isMultiOrgMember(ctx context.Context, client *coder.Client, email string) (bool, error) {
u, err := client.UserByEmail(ctx, email)
if err != nil {
return false, xerrors.New("email not found")
}

orgs, err := client.Organizations(ctx)
if err != nil {
return false, err
}
return len(lookupUserOrgs(u, orgs)) > 1, nil
}
Loading