Skip to content

Commit 440a058

Browse files
committed
Merge branch 'main' into org-members-page
2 parents e90d7c3 + 6058bcd commit 440a058

38 files changed

+635
-137
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ jobs:
170170
171171
# Check for any typos
172172
- name: Check for typos
173-
uses: crate-ci/typos@v1.23.1
173+
uses: crate-ci/typos@v1.23.2
174174
with:
175175
config: .github/workflows/typos.toml
176176

.github/workflows/security.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ jobs:
114114
echo "image=$(cat "$image_job")" >> $GITHUB_OUTPUT
115115
116116
- name: Run Trivy vulnerability scanner
117-
uses: aquasecurity/trivy-action@7c2007bcb556501da015201bcba5aa14069b74e2
117+
uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8
118118
with:
119119
image-ref: ${{ steps.build.outputs.image }}
120120
format: sarif

cli/create.go

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"strings"
78
"time"
89

910
"github.com/google/uuid"
@@ -29,7 +30,9 @@ func (r *RootCmd) create() *serpent.Command {
2930
parameterFlags workspaceParameterFlags
3031
autoUpdates string
3132
copyParametersFrom string
32-
orgContext = NewOrganizationContext()
33+
// Organization context is only required if more than 1 template
34+
// shares the same name across multiple organizations.
35+
orgContext = NewOrganizationContext()
3336
)
3437
client := new(codersdk.Client)
3538
cmd := &serpent.Command{
@@ -44,11 +47,7 @@ func (r *RootCmd) create() *serpent.Command {
4447
),
4548
Middleware: serpent.Chain(r.InitClient(client)),
4649
Handler: func(inv *serpent.Invocation) error {
47-
organization, err := orgContext.Selected(inv, client)
48-
if err != nil {
49-
return err
50-
}
51-
50+
var err error
5251
workspaceOwner := codersdk.Me
5352
if len(inv.Args) >= 1 {
5453
workspaceOwner, workspaceName, err = splitNamedWorkspace(inv.Args[0])
@@ -99,7 +98,7 @@ func (r *RootCmd) create() *serpent.Command {
9998
if templateName == "" {
10099
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select a template below to preview the provisioned infrastructure:"))
101100

102-
templates, err := client.TemplatesByOrganization(inv.Context(), organization.ID)
101+
templates, err := client.Templates(inv.Context(), codersdk.TemplateFilter{})
103102
if err != nil {
104103
return err
105104
}
@@ -111,13 +110,28 @@ func (r *RootCmd) create() *serpent.Command {
111110
templateNames := make([]string, 0, len(templates))
112111
templateByName := make(map[string]codersdk.Template, len(templates))
113112

113+
// If more than 1 organization exists in the list of templates,
114+
// then include the organization name in the select options.
115+
uniqueOrganizations := make(map[uuid.UUID]bool)
116+
for _, template := range templates {
117+
uniqueOrganizations[template.OrganizationID] = true
118+
}
119+
114120
for _, template := range templates {
115121
templateName := template.Name
122+
if len(uniqueOrganizations) > 1 {
123+
templateName += cliui.Placeholder(
124+
fmt.Sprintf(
125+
" (%s)",
126+
template.OrganizationName,
127+
),
128+
)
129+
}
116130

117131
if template.ActiveUserCount > 0 {
118132
templateName += cliui.Placeholder(
119133
fmt.Sprintf(
120-
" (used by %s)",
134+
" used by %s",
121135
formatActiveDevelopers(template.ActiveUserCount),
122136
),
123137
)
@@ -145,13 +159,65 @@ func (r *RootCmd) create() *serpent.Command {
145159
}
146160
templateVersionID = sourceWorkspace.LatestBuild.TemplateVersionID
147161
} else {
148-
template, err = client.TemplateByName(inv.Context(), organization.ID, templateName)
162+
templates, err := client.Templates(inv.Context(), codersdk.TemplateFilter{
163+
ExactName: templateName,
164+
})
149165
if err != nil {
150166
return xerrors.Errorf("get template by name: %w", err)
151167
}
168+
if len(templates) == 0 {
169+
return xerrors.Errorf("no template found with the name %q", templateName)
170+
}
171+
172+
if len(templates) > 1 {
173+
templateOrgs := []string{}
174+
for _, tpl := range templates {
175+
templateOrgs = append(templateOrgs, tpl.OrganizationName)
176+
}
177+
178+
selectedOrg, err := orgContext.Selected(inv, client)
179+
if err != nil {
180+
return xerrors.Errorf("multiple templates found with the name %q, use `--org=<organization_name>` to specify which template by that name to use. Organizations available: %s", templateName, strings.Join(templateOrgs, ", "))
181+
}
182+
183+
index := slices.IndexFunc(templates, func(i codersdk.Template) bool {
184+
return i.OrganizationID == selectedOrg.ID
185+
})
186+
if index == -1 {
187+
return xerrors.Errorf("no templates found with the name %q in the organization %q. Templates by that name exist in organizations: %s. Use --org=<organization_name> to select one.", templateName, selectedOrg.Name, strings.Join(templateOrgs, ", "))
188+
}
189+
190+
// remake the list with the only template selected
191+
templates = []codersdk.Template{templates[index]}
192+
}
193+
194+
template = templates[0]
152195
templateVersionID = template.ActiveVersionID
153196
}
154197

198+
// If the user specified an organization via a flag or env var, the template **must**
199+
// be in that organization. Otherwise, we should throw an error.
200+
orgValue, orgValueSource := orgContext.ValueSource(inv)
201+
if orgValue != "" && !(orgValueSource == serpent.ValueSourceDefault || orgValueSource == serpent.ValueSourceNone) {
202+
selectedOrg, err := orgContext.Selected(inv, client)
203+
if err != nil {
204+
return err
205+
}
206+
207+
if template.OrganizationID != selectedOrg.ID {
208+
orgNameFormat := "'--org=%q'"
209+
if orgValueSource == serpent.ValueSourceEnv {
210+
orgNameFormat = "CODER_ORGANIZATION=%q"
211+
}
212+
213+
return xerrors.Errorf("template is in organization %q, but %s was specified. Use %s to use this template",
214+
template.OrganizationName,
215+
fmt.Sprintf(orgNameFormat, selectedOrg.Name),
216+
fmt.Sprintf(orgNameFormat, template.OrganizationName),
217+
)
218+
}
219+
}
220+
155221
var schedSpec *string
156222
if startAt != "" {
157223
sched, err := parseCLISchedule(startAt)
@@ -207,7 +273,7 @@ func (r *RootCmd) create() *serpent.Command {
207273
ttlMillis = ptr.Ref(stopAfter.Milliseconds())
208274
}
209275

210-
workspace, err := client.CreateWorkspace(inv.Context(), organization.ID, workspaceOwner, codersdk.CreateWorkspaceRequest{
276+
workspace, err := client.CreateWorkspace(inv.Context(), template.OrganizationID, workspaceOwner, codersdk.CreateWorkspaceRequest{
211277
TemplateVersionID: templateVersionID,
212278
Name: workspaceName,
213279
AutostartSchedule: schedSpec,

cli/prompts.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,45 @@ func (RootCmd) promptExample() *serpent.Command {
100100
}
101101
return err
102102
}, useSearchOption),
103+
promptCmd("multiple", func(inv *serpent.Invocation) error {
104+
_, _ = fmt.Fprintf(inv.Stdout, "This command exists to test the behavior of multiple prompts. The survey library does not erase the original message prompt after.")
105+
thing, err := cliui.Select(inv, cliui.SelectOptions{
106+
Message: "Select a thing",
107+
Options: []string{
108+
"Car", "Bike", "Plane", "Boat", "Train",
109+
},
110+
Default: "Car",
111+
})
112+
if err != nil {
113+
return err
114+
}
115+
color, err := cliui.Select(inv, cliui.SelectOptions{
116+
Message: "Select a color",
117+
Options: []string{
118+
"Blue", "Green", "Yellow", "Red",
119+
},
120+
Default: "Blue",
121+
})
122+
if err != nil {
123+
return err
124+
}
125+
properties, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
126+
Message: "Select properties",
127+
Options: []string{
128+
"Fast", "Cool", "Expensive", "New",
129+
},
130+
Defaults: []string{"Fast"},
131+
})
132+
if err != nil {
133+
return err
134+
}
135+
_, _ = fmt.Fprintf(inv.Stdout, "Your %s %s is awesome! Did you paint it %s?\n",
136+
strings.Join(properties, " "),
137+
thing,
138+
color,
139+
)
140+
return err
141+
}),
103142
promptCmd("multi-select", func(inv *serpent.Invocation) error {
104143
values, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
105144
Message: "Select some things:",

cli/root.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,9 +641,10 @@ func NewOrganizationContext() *OrganizationContext {
641641
return &OrganizationContext{}
642642
}
643643

644+
func (*OrganizationContext) optionName() string { return "Organization" }
644645
func (o *OrganizationContext) AttachOptions(cmd *serpent.Command) {
645646
cmd.Options = append(cmd.Options, serpent.Option{
646-
Name: "Organization",
647+
Name: o.optionName(),
647648
Description: "Select which organization (uuid or name) to use.",
648649
// Only required if the user is a part of more than 1 organization.
649650
// Otherwise, we can assume a default value.
@@ -655,6 +656,14 @@ func (o *OrganizationContext) AttachOptions(cmd *serpent.Command) {
655656
})
656657
}
657658

659+
func (o *OrganizationContext) ValueSource(inv *serpent.Invocation) (string, serpent.ValueSource) {
660+
opt := inv.Command.Options.ByName(o.optionName())
661+
if opt == nil {
662+
return o.FlagSelect, serpent.ValueSourceNone
663+
}
664+
return o.FlagSelect, opt.ValueSource
665+
}
666+
658667
func (o *OrganizationContext) Selected(inv *serpent.Invocation, client *codersdk.Client) (codersdk.Organization, error) {
659668
// Fetch the set of organizations the user is a member of.
660669
orgs, err := client.OrganizationsByUser(inv.Context(), codersdk.Me)

cli/templatecreate.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func (r *RootCmd) templateCreate() *serpent.Command {
160160
RequireActiveVersion: requireActiveVersion,
161161
}
162162

163-
_, err = client.CreateTemplate(inv.Context(), organization.ID, createReq)
163+
template, err := client.CreateTemplate(inv.Context(), organization.ID, createReq)
164164
if err != nil {
165165
return err
166166
}
@@ -171,7 +171,7 @@ func (r *RootCmd) templateCreate() *serpent.Command {
171171
pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))+"! "+
172172
"Developers can provision a workspace with this template using:")+"\n")
173173

174-
_, _ = fmt.Fprintln(inv.Stdout, " "+pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("coder create --template=%q [workspace name]", templateName)))
174+
_, _ = fmt.Fprintln(inv.Stdout, " "+pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("coder create --template=%q --org=%q [workspace name]", templateName, template.OrganizationName)))
175175
_, _ = fmt.Fprintln(inv.Stdout)
176176

177177
return nil
@@ -244,6 +244,7 @@ func (r *RootCmd) templateCreate() *serpent.Command {
244244

245245
cliui.SkipPromptOption(),
246246
}
247+
orgContext.AttachOptions(cmd)
247248
cmd.Options = append(cmd.Options, uploadFlags.options()...)
248249
return cmd
249250
}

cli/testdata/coder_templates_create_--help.golden

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ USAGE:
77
flag
88

99
OPTIONS:
10+
-O, --org string, $CODER_ORGANIZATION
11+
Select which organization (uuid or name) to use.
12+
1013
--default-ttl duration (default: 24h)
1114
Specify a default TTL for workspaces created from this template. It is
1215
the default time before shutdown - workspaces created from this

coderd/apidoc/docs.go

Lines changed: 6 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apikey.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ func (api *API) deleteAPIKey(rw http.ResponseWriter, r *http.Request) {
333333
return
334334
}
335335

336-
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
336+
rw.WriteHeader(http.StatusNoContent)
337337
}
338338

339339
// @Summary Get token config

coderd/database/db2sdk/db2sdk.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -491,13 +491,14 @@ func Apps(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent, ownerNa
491491

492492
func ProvisionerDaemon(dbDaemon database.ProvisionerDaemon) codersdk.ProvisionerDaemon {
493493
result := codersdk.ProvisionerDaemon{
494-
ID: dbDaemon.ID,
495-
CreatedAt: dbDaemon.CreatedAt,
496-
LastSeenAt: codersdk.NullTime{NullTime: dbDaemon.LastSeenAt},
497-
Name: dbDaemon.Name,
498-
Tags: dbDaemon.Tags,
499-
Version: dbDaemon.Version,
500-
APIVersion: dbDaemon.APIVersion,
494+
ID: dbDaemon.ID,
495+
OrganizationID: dbDaemon.OrganizationID,
496+
CreatedAt: dbDaemon.CreatedAt,
497+
LastSeenAt: codersdk.NullTime{NullTime: dbDaemon.LastSeenAt},
498+
Name: dbDaemon.Name,
499+
Tags: dbDaemon.Tags,
500+
Version: dbDaemon.Version,
501+
APIVersion: dbDaemon.APIVersion,
501502
}
502503
for _, provisionerType := range dbDaemon.Provisioners {
503504
result.Provisioners = append(result.Provisioners, codersdk.ProvisionerType(provisionerType))

coderd/debug.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ func (api *API) putDeploymentHealthSettings(rw http.ResponseWriter, r *http.Requ
235235

236236
if bytes.Equal(settingsJSON, []byte(currentSettingsJSON)) {
237237
// See: https://www.rfc-editor.org/rfc/rfc7231#section-6.3.5
238-
httpapi.Write(r.Context(), rw, http.StatusNoContent, nil)
238+
rw.WriteHeader(http.StatusNoContent)
239239
return
240240
}
241241

coderd/externalauth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func (api *API) postExternalAuthDeviceByID(rw http.ResponseWriter, r *http.Reque
197197
return
198198
}
199199
}
200-
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
200+
rw.WriteHeader(http.StatusNoContent)
201201
}
202202

203203
// @Summary Get external auth device by ID.

coderd/identityprovider/revoke.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ func RevokeApp(db database.Store) http.HandlerFunc {
3939
httpapi.InternalServerError(rw, err)
4040
return
4141
}
42-
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
42+
rw.WriteHeader(http.StatusNoContent)
4343
}
4444
}

0 commit comments

Comments
 (0)