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

Commit dee3f7c

Browse files
author
Faris Huskovic
committed
Change coder create | edit envs image flag to take image name and use defaults from image
1 parent 6f8b9b8 commit dee3f7c

File tree

5 files changed

+333
-81
lines changed

5 files changed

+333
-81
lines changed

Diff for: ci/integration/envs_test.go

+13-13
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
)
1010

1111
// From Coder organization images
12-
const ubuntuImgID = "5f443b16-30652892427b955601330fa5"
12+
// const ubuntuImgID = "5f443b16-30652892427b955601330fa5"
1313

1414
func TestEnvsCLI(t *testing.T) {
1515
t.Parallel()
@@ -37,6 +37,18 @@ func TestEnvsCLI(t *testing.T) {
3737
tcli.StderrEmpty(),
3838
)
3939

40+
// Image unset.
41+
c.Run(ctx, "coder envs create test-env").Assert(t,
42+
tcli.StderrMatches(regexp.QuoteMeta("fatal: required flag(s) \"image\" not set")),
43+
tcli.Error(),
44+
)
45+
46+
// Image not imported.
47+
c.Run(ctx, "coder envs create test-env --image doesntmatter").Assert(t,
48+
tcli.StderrMatches(regexp.QuoteMeta("fatal: image not found - did you forget to import this image?")),
49+
tcli.Error(),
50+
)
51+
4052
// TODO(Faris) : uncomment this when we can safely purge the environments
4153
// the integrations tests would create in the sidecar
4254
// Successfully create environment.
@@ -46,24 +58,12 @@ func TestEnvsCLI(t *testing.T) {
4658
// tcli.StderrMatches(regexp.QuoteMeta("Successfully created environment \"test-ubuntu\"")),
4759
// )
4860

49-
// Invalid environment name should fail.
50-
c.Run(ctx, "coder envs create --image "+ubuntuImgID+" this-IS-an-invalid-EnvironmentName").Assert(t,
51-
tcli.Error(),
52-
tcli.StderrMatches(regexp.QuoteMeta("environment name must conform to regex ^[a-z0-9]([a-z0-9-]+)?")),
53-
)
54-
5561
// TODO(Faris) : uncomment this when we can safely purge the environments
5662
// the integrations tests would create in the sidecar
5763
// Successfully provision environment with fractional resource amounts
5864
// c.Run(ctx, fmt.Sprintf(`coder envs create -i %s -c 1.2 -m 1.4 non-whole-resource-amounts`, ubuntuImgID)).Assert(t,
5965
// tcli.Success(),
6066
// tcli.StderrMatches(regexp.QuoteMeta("Successfully created environment \"non-whole-resource-amounts\"")),
6167
// )
62-
63-
// Image does not exist should fail.
64-
c.Run(ctx, "coder envs create --image does-not-exist env-will-not-be-created").Assert(t,
65-
tcli.Error(),
66-
tcli.StderrMatches(regexp.QuoteMeta("does not exist")),
67-
)
6868
})
6969
}

Diff for: coder-sdk/image.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type Image struct {
1313
Description string `json:"description"`
1414
URL string `json:"url"` // User-supplied URL for image.
1515
DefaultCPUCores float32 `json:"default_cpu_cores"`
16-
DefaultMemoryGB int `json:"default_memory_gb"`
16+
DefaultMemoryGB float32 `json:"default_memory_gb"`
1717
DefaultDiskGB int `json:"default_disk_gb"`
1818
Deprecated bool `json:"deprecated"`
1919
}
@@ -47,3 +47,12 @@ func (c Client) ImportImage(ctx context.Context, orgID string, req ImportImageRe
4747
}
4848
return &img, nil
4949
}
50+
51+
// OrganizationImages returns all of the images imported for orgID.
52+
func (c Client) OrganizationImages(ctx context.Context, orgID string) ([]Image, error) {
53+
var imgs []Image
54+
if err := c.requestBody(ctx, http.MethodGet, "/api/orgs/"+orgID+"/images", nil, &imgs); err != nil {
55+
return nil, err
56+
}
57+
return imgs, nil
58+
}

Diff for: internal/clog/error.go

+10
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ func LogSuccess(header string, lines ...string) {
6868
}.String())
6969
}
7070

71+
// LogWarn prints the given warn message to stderr.
72+
func LogWarn(header string, lines ...string) {
73+
fmt.Fprint(os.Stderr, CLIMessage{
74+
Level: "warning",
75+
Color: color.FgYellow,
76+
Header: header,
77+
Lines: lines,
78+
}.String())
79+
}
80+
7181
// Warn creates an error with the level "warning".
7282
func Warn(header string, lines ...string) CLIError {
7383
return CLIError{

Diff for: internal/cmd/ceapi.go

+109
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
"cdr.dev/coder-cli/coder-sdk"
89
"cdr.dev/coder-cli/internal/clog"
@@ -81,3 +82,111 @@ func findEnv(ctx context.Context, client *coder.Client, envName, userEmail strin
8182
clog.Tipf("run \"coder envs ls\" to view your environments"),
8283
)
8384
}
85+
86+
type findImgConf struct {
87+
client *coder.Client
88+
email string
89+
imgName string
90+
orgName string
91+
}
92+
93+
func findImg(ctx context.Context, conf findImgConf) (*coder.Image, error) {
94+
switch {
95+
case conf.email == "":
96+
return nil, xerrors.New("user email unset")
97+
case conf.imgName == "":
98+
return nil, xerrors.New("image name unset")
99+
}
100+
101+
imgs, err := getImgs(ctx,
102+
getImgsConf{
103+
client: conf.client,
104+
email: conf.email,
105+
orgName: conf.orgName,
106+
},
107+
)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
var possibleMatches []coder.Image
113+
114+
// The user may provide an image thats not an exact match
115+
// to one of their imported images but they may be close.
116+
// We can assist the user by collecting images that contain
117+
// the user provided image flag value as a substring.
118+
for _, img := range imgs {
119+
// If it's an exact match we can just return and exit.
120+
if img.Repository == conf.imgName {
121+
return &img, nil
122+
}
123+
if strings.Contains(img.Repository, conf.imgName) {
124+
possibleMatches = append(possibleMatches, img)
125+
}
126+
}
127+
128+
if len(possibleMatches) == 0 {
129+
return nil, xerrors.New("image not found - did you forget to import this image?")
130+
}
131+
132+
lines := []string{clog.Tipf("Did you mean?")}
133+
134+
for _, img := range possibleMatches {
135+
lines = append(lines, img.Repository)
136+
}
137+
return nil, clog.Fatal(
138+
fmt.Sprintf("Found %d possible matches for %q.", len(possibleMatches), conf.imgName),
139+
lines...,
140+
)
141+
}
142+
143+
type getImgsConf struct {
144+
client *coder.Client
145+
email string
146+
orgName string
147+
}
148+
149+
func getImgs(ctx context.Context, conf getImgsConf) ([]coder.Image, error) {
150+
u, err := conf.client.UserByEmail(ctx, conf.email)
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
orgs, err := conf.client.Organizations(ctx)
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
orgs = lookupUserOrgs(u, orgs)
161+
162+
for _, org := range orgs {
163+
imgs, err := conf.client.OrganizationImages(ctx, org.ID)
164+
if err != nil {
165+
return nil, err
166+
}
167+
// If orgName is set we know the user is a multi-org member
168+
// so we should only return the imported images that beong to the org they specified.
169+
if conf.orgName != "" && conf.orgName == org.Name {
170+
return imgs, nil
171+
}
172+
173+
if conf.orgName == "" {
174+
// if orgName is unset we know the user is only part of one org.
175+
return imgs, nil
176+
}
177+
}
178+
return nil, xerrors.Errorf("org name %q not found", conf.orgName)
179+
}
180+
181+
func isMultiOrgMember(ctx context.Context, client *coder.Client, email string) (bool, error) {
182+
u, err := client.UserByEmail(ctx, email)
183+
if err != nil {
184+
return false, xerrors.New("email not found")
185+
}
186+
187+
orgs, err := client.Organizations(ctx)
188+
if err != nil {
189+
return false, err
190+
}
191+
return len(lookupUserOrgs(u, orgs)) > 1, nil
192+
}

0 commit comments

Comments
 (0)