Skip to content

Commit 9cbe2b2

Browse files
authored
chore: create workspaces and templates for multiple orgs (#13866)
* chore: creating workspaces and templates to work with orgs * handle wrong org selected * create org member in coderdtest helper
1 parent e4aef27 commit 9cbe2b2

File tree

9 files changed

+312
-16
lines changed

9 files changed

+312
-16
lines changed

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/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/searchquery/search.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,8 @@ func Templates(ctx context.Context, db database.Store, query string) (database.G
198198

199199
parser := httpapi.NewQueryParamParser()
200200
filter := database.GetTemplatesWithFilterParams{
201-
Deleted: parser.Boolean(values, false, "deleted"),
202-
// TODO: Should name be a fuzzy search?
203-
ExactName: parser.String(values, "", "name"),
201+
Deleted: parser.Boolean(values, false, "deleted"),
202+
ExactName: parser.String(values, "", "exact_name"),
204203
IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"),
205204
Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"),
206205
}

codersdk/organizations.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui
365365

366366
type TemplateFilter struct {
367367
OrganizationID uuid.UUID
368+
ExactName string
368369
}
369370

370371
// asRequestOption returns a function that can be used in (*Client).Request.
@@ -378,6 +379,10 @@ func (f TemplateFilter) asRequestOption() RequestOption {
378379
params = append(params, fmt.Sprintf("organization:%q", f.OrganizationID.String()))
379380
}
380381

382+
if f.ExactName != "" {
383+
params = append(params, fmt.Sprintf("exact_name:%q", f.ExactName))
384+
}
385+
381386
q := r.URL.Query()
382387
q.Set("q", strings.Join(params, " "))
383388
r.URL.RawQuery = q.Encode()

docs/cli/templates_create.md

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

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy